refactor: move skill to skills/inkreach-teable (from .agents/skills/)
git mv rename (history preserved); install.sh + README updated. The skill is consumed via user-level install (~/.zcode/skills), so in-repo .agents discovery was unnecessary.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: inkreach-teable
|
||||
description: >-
|
||||
Operate a deployed inkreach-teable instance (a Teable fork with a self-built
|
||||
automation engine) via the inkreach-teable CLI: configure tables, fields,
|
||||
records, views, and automations (8 triggers incl. recordCreated/Updated/
|
||||
Condition, formSubmitted, scheduledTime, buttonClick, webhook; 7 actions),
|
||||
inspect field constraints (select choices, visibilityFilter conditions),
|
||||
manage secrets, execution history, plugins, and record subscriptions. Use
|
||||
when the user references Teable-style IDs (bseXXX, tblXXX, fldXXX, recXXX,
|
||||
viwXXX, wflXXX) or wants to configure tables/automations/plugins on the
|
||||
deployed instance — even without saying "inkreach-teable". The official
|
||||
teable CLI does NOT work here (different automation contract).
|
||||
---
|
||||
|
||||
# inkreach-teable operations
|
||||
|
||||
Operate a deployed inkreach-teable instance via the `inkreach-teable` CLI.
|
||||
Respond in the user's language. Keep answers concise and action-oriented.
|
||||
|
||||
> **Critical**: the automation engine is **self-built and differs from official
|
||||
> teable** — the official `teable` CLI corrupts or 404s on automations here.
|
||||
> Exact key-name differences are in [Key Constraints](#3-key-constraints).
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
- Install the CLI if missing: `bash scripts/install.sh` (relative to this skill dir).
|
||||
- **Configure once**: `inkreach-teable auth login --endpoint http://localhost:3003 --token <PAT> [--base-id bseXXX]`; verify with `auth status`. PAT comes from `auth token create --name x` (full token shown **once**) or the web UI (Settings → Personal Access Tokens).
|
||||
- `--json` on any command returns raw JSON (for agent parsing). Fallback for unknown commands: `inkreach-teable <cmd> --help`.
|
||||
- **Read on demand**: consult the guide linked for your module when doing non-trivial work — no need to read everything up front.
|
||||
|
||||
## 2. Module & Command Navigation
|
||||
|
||||
| Module | What it solves | Entry commands | Detail doc |
|
||||
|--------|---------------|----------------|------------|
|
||||
| Tables & Fields | Create/modify structure; **inspect select-choice constraints** | `table create`, `field create`, `field list --with-options`, `field get` | [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` | [tables-fields.md § Records](guides/tables-fields.md) |
|
||||
| Views | Filtered/sorted perspectives | `view create/update` | [tables-fields.md § Views](guides/tables-fields.md) |
|
||||
| View presets | View-scoped shared filter/sort bundles + per-user personal config, applied via toolbar | `preset create/update/list/delete` | [tables-fields.md § View Presets](guides/tables-fields.md) |
|
||||
| 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 | API keys for script actions | `secret set/list/delete` | [automation-guide.md § Secrets](guides/automation-guide.md) |
|
||||
| Execution History | Run results / rerun failures | `runs list/get/rerun` | [automation-guide.md § Testing](guides/automation-guide.md) |
|
||||
| Manual Trigger | Test button-click/webhook | `fire button-click`, `fire webhook` | [automation-guide.md § Testing](guides/automation-guide.md) |
|
||||
| SQL | Read-only analytics, JOINs | `sql '<sql>'` | [cli-reference.md § sql](guides/cli-reference.md) |
|
||||
| Plugins | Dynamic plugins, dev whitelist, CSP | `plugin *` | [cli-reference.md § plugin](guides/cli-reference.md) |
|
||||
| Subscriptions | Record digest/instant email | `subscription *` | [subscription.md](guides/subscription.md) |
|
||||
| Dev/Deploy | Build, restart, gotchas | systemd / pnpm | [development.md](guides/development.md) |
|
||||
|
||||
Full command syntax: [cli-reference.md](guides/cli-reference.md). Instance access & credentials: [instance-access.md](guides/instance-access.md).
|
||||
|
||||
### Routing rules
|
||||
|
||||
| User intent | Correct approach | Do NOT |
|
||||
|-------------|------------------|--------|
|
||||
| Configure an automation | `workflow create` → `trigger set` → `action add` → `activate` | Editing DB rows |
|
||||
| React to new records / field changes | `recordCreated` / `recordUpdated --field-ids` | Polling in a script; `recordCondition` ignores fields |
|
||||
| React to a condition matching | Trigger `recordCondition` (NOT recordMatchesConditions) | Official-teable key names |
|
||||
| Button click starts a workflow | `buttonClick` trigger + Button field (`options.workflow` object) | Other trigger types |
|
||||
| Receive external HTTP | `webhook`; test via `fire webhook` | Expecting auth (backend ignores it) |
|
||||
| Schedule recurring job | `scheduledTime` (only `timing.type` + `interval` honored) | triggerTime/weekdays (ignored) |
|
||||
| Per-row transformation | `script` action reading `triggerInput` | Row-by-row record API |
|
||||
| Send notification | `sendNotification` (in-app) | `sendEmail` (needs SMTP) unless email required |
|
||||
| **Inspect select-choice constraints** | `field list --with-options`, `field get <tbl> <name>`, `field visible-choices` | `--json` + manual filtering |
|
||||
| Read records for later writes | `record get` (returns record IDs) | `sql` (no record IDs) |
|
||||
| Analytics / JOIN | `sql` with GROUP BY | Fetch all records + compute |
|
||||
| Store an API key for scripts | `secret set` → `process.env.NAME` | Hardcoding |
|
||||
| Workflow didn't run | `runs list --status failed` → `runs get` | Blind re-activate |
|
||||
|
||||
## 3. Key Constraints
|
||||
|
||||
These differ from official teable and from the (stale) `docs/automation/triggers.md`.
|
||||
|
||||
1. **Filter key is `conditions`** (NOT `filter`): `trigger set --conditions '<json>'`, shape `{conjunction, filterSet:[{fieldId, operator, value}]}`.
|
||||
2. **Watch-fields key is `fieldIds`** (NOT `watchFieldIds`); `[]` = watch all fields.
|
||||
3. **Trigger category `recordCondition`** (NOT `recordMatchesConditions`).
|
||||
4. **`scheduledTime` honors only `timing.type` + `timing.interval`** — triggerTime/weekdays/daysOfMonth/starting/ending/tz are ignored; `weeks` is hardcoded to Sunday.
|
||||
5. **Webhook route is `/api/webhook/{baseId}/{workflowId}`** (no `/base/` segment); `authorization` config is UI-only, backend never validates. Fire: `fire webhook <baseId> <wfId>`.
|
||||
6. **Node updates are whole-graph replacement** — `trigger set` / `action add/update/delete` do GET→modify→PUT internally; never hand-edit partial graphs.
|
||||
7. **`activate` creates a snapshot** — node changes take effect only after re-activating. `hasDraft: true` = unactivated changes.
|
||||
8. **`workflow delete` is soft** (cascades to nodes, tokens, trigger unregister); test workflows should be `deactivate`d.
|
||||
9. **Secrets are write-only** — `secret list` never returns values; scripts read `process.env.<NAME>`.
|
||||
10. **Button field binding**: `options.workflow` is an **object** `{id, name?}`, not a bare id string. The field's `isActive` snapshot can go stale; the API validates the workflow's DB `isActive` at click time (deactivated → 400).
|
||||
11. **Script sandbox** has NO `fs`/`eval`/`Function`, but has `triggerInput`, `console`, `fetch`, `process.env` (secrets + `AUTOMATION_TOKEN` + `PUBLIC_ORIGIN`), and a controlled `require`. Call back into the instance via `triggerInput._accessToken` + `triggerInput._apiBaseUrl`. **npm deps**: declare with `action add/update --dependencies '[{"name":"axios","version":"1.6.0"}]'` (or `axios@1.6.0,dayjs`). Requires `npm` on the backend **service** PATH — systemd user units default to a PATH that omits the nvm bin dir, so deps fail with `spawn npm ENOENT`; fix by adding `Environment=PATH=<nvm-bin>:/usr/local/sbin:...` to `teable-backend.service`.
|
||||
12. **Field options updates go through the convert API** — `field update --options` handles this; the plain PATCH update API strips `options`.
|
||||
|
||||
## 4. Quick Syntax
|
||||
|
||||
```bash
|
||||
inkreach-teable auth login --endpoint http://localhost:3003 --token <PAT> # once
|
||||
inkreach-teable table create --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"singleSelect","options":{"choices":["Todo","Done"]}}]'
|
||||
inkreach-teable workflow create --name "On Go click" # → wflXXX
|
||||
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
|
||||
inkreach-teable fire button-click tblXXX recXXX fldXXX && inkreach-teable runs list wflXXX
|
||||
```
|
||||
|
||||
## 5. Common Errors & Recovery
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `MISSING_BASE_ID` | No base id resolved | Pass `--base-id`, or `auth login --base-id` |
|
||||
| `401` / access denied | Token invalid/expired or lacks scope | `auth status`; recreate PAT |
|
||||
| `403` | Base role too low | Grant owner/creator role in web UI |
|
||||
| Workflow never fires | Not activated / trigger misconfigured | `workflow get` → `isActive`; verify trigger type + tableId/fieldIds |
|
||||
| `scheduledTime` wrong time | Backend ignores most timing fields | Only `timing.type`+`interval` work |
|
||||
| button-click 400 "not active" | Workflow deactivated since field created | `workflow activate <id>` |
|
||||
| `runs list` shows failed | Action error | `runs get <wfId> <execId>` → `nodeResults[].error` |
|
||||
| Script deps fail `spawn npm ENOENT` | `npm` not on service PATH | See Key Constraint 11 |
|
||||
| Record write 400 "does not meet the current row conditions" | Value violates choice `visibilityFilter` | `field visible-choices` to see allowed options |
|
||||
@@ -0,0 +1,333 @@
|
||||
# Action Types Reference
|
||||
|
||||
All 7 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 7 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`.
|
||||
|
||||
**Trigger user role** (user-initiated triggers only — buttonClick / recordCreated /
|
||||
recordUpdated / recordCondition / formSubmitted): when a real user fires the
|
||||
workflow, `triggerInput` also carries their role in THIS base:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|-------|------|---------|
|
||||
| `triggerInput.user` | `{id,name,email}` | Who triggered (absent for scheduledTime/webhook) |
|
||||
| `triggerInput._triggerUserRoles` | `string[]` | Display names of the user's enabled, non-deleted roles in this base. `[]` for admins (admins have no AM role) |
|
||||
| `triggerInput._triggerUserIsAdmin` | `boolean` | `true` for space owner, AM admin users, or everyone when AM is disabled for the base |
|
||||
|
||||
These fields are **absent** for system triggers (scheduledTime / webhook) — no
|
||||
user context. Branch on them in scripts:
|
||||
```js
|
||||
const roles = triggerInput._triggerUserRoles ?? [];
|
||||
const isAdmin = triggerInput._triggerUserIsAdmin ?? false;
|
||||
if (isAdmin) { /* admin path */ }
|
||||
else if (roles.includes('工厂运营')) { /* operator path */ }
|
||||
else if (roles.includes('售后客服')) { /* support path */ }
|
||||
```
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**With npm dependencies** (installed into a sandbox cache, then `require()`-able):
|
||||
```bash
|
||||
# JSON form (recommended)
|
||||
inkreach-teable action add <wfId> --type script \
|
||||
--code-file ./sync.js \
|
||||
--dependencies '[{"name":"axios","version":"1.6.0"},{"name":"dayjs","version":"1.11.10"}]'
|
||||
# shorthand: name@version,name (version omitted → 'latest')
|
||||
inkreach-teable action add <wfId> --type script --code-file ./sync.js --dependencies 'axios@1.6.0,dayjs'
|
||||
# replace / clear deps on an existing node
|
||||
inkreach-teable action update <wfId> <nodeId> --dependencies '[{"name":"axios"}]'
|
||||
inkreach-teable action update <wfId> <nodeId> --dependencies '[]' # clear
|
||||
```
|
||||
|
||||
> ⚠️ **Deployment requirement**: the backend installs deps via `spawn('npm')`, so `npm` must be on the **service** PATH. systemd user units default to a PATH that omits the nvm bin dir → scripts with deps fail with `spawn npm ENOENT`. Fix: add `Environment=PATH=<nvm-bin>:/usr/local/sbin:...` to `teable-backend.service`.
|
||||
|
||||
**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. Install is cached by `name@version` hash; first run downloads, later runs reuse. CLI accepts JSON `[{"name","version?"}]` or shorthand `name@version,name`. `update` replaces the whole array (not append).
|
||||
|
||||
> ⚠️ **`--config` must be the FLAT action config itself** — the JSON above, exactly as shown. It is assigned directly to `node.config` (CLI does `config = JSON.parse(opts.config)`).
|
||||
> Do **NOT** wrap it in a node envelope like `{"config": {...}}` or `{"type": "script", "config": {...}}` — the engine then reads `config.code` as `undefined` and the node becomes a **silent empty script**: it "succeeds" in ~1ms with empty stdout and `result: {}`, and every downstream `{{nodeId.result.*}}` variable reference breaks (e.g. sendNotification fails with `Missing required fields: message`).
|
||||
>
|
||||
> Correct: `--config '{"code":"return 1","timeout":120000}'` · Wrong: `--config '{"config":{"code":"return 1"}}'`
|
||||
>
|
||||
> **Verify after every `--config` deploy** (also guards against the automation editor overwriting CLI changes with a stale draft — close the editor before CLI updates):
|
||||
> ```bash
|
||||
> inkreach-teable workflow get <wfId> --base-id <bseId> --json | python3 -c "
|
||||
> import json,sys
|
||||
> d=json.load(sys.stdin)
|
||||
> for n in d.get('nodes',[]):
|
||||
> if n.get('id')=='<nodeId>':
|
||||
> c=n.get('config') or {}
|
||||
> print('keys:',list(c.keys()),'code-len:',len(c.get('code') or ''))"
|
||||
> # expect keys ['code','timeout',...] and code-len > 0
|
||||
> ```
|
||||
> Symptom cheat-sheet: script "succeeded" but `executionTime` ≈ 1ms, `stdout` empty, `result` `{}` → config shape was wrong (code never deployed).
|
||||
|
||||
**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 '{{n0.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> }`
|
||||
|
||||
---
|
||||
|
||||
## 4. sendNotification — System in-app notification (站内信,非邮件)
|
||||
|
||||
```bash
|
||||
inkreach-teable action add <wfId> --type sendNotification \
|
||||
--to-user usr4ioKJl6XpZgyFhxW \
|
||||
--title '新工单' --message '工单 {{n0.record.id}} 已创建' \
|
||||
--severity info
|
||||
```
|
||||
|
||||
**Config:**
|
||||
```json
|
||||
{
|
||||
"toUserId": "usr4ioKJl6XpZgyFhxW",
|
||||
"title": "新工单",
|
||||
"message": "工单 {{n0.record.id}} 已创建",
|
||||
"path": "/base/bseX/table/tblY",
|
||||
"severity": "info"
|
||||
}
|
||||
```
|
||||
- `toUserId`: recipient user ID. Can reference the trigger user with `{{n0.user.id}}` (submitter/operator — only populated on user-initiated triggers; scheduled/webhook resolve to empty).
|
||||
- `title` + `message`: notification text; both support `{{nodeId.path}}` variables (or `{{index.path}}` — both formats resolve identically). If title set, rendered as `title\nmessage`.
|
||||
- `path` (optional): click-through URL in the notification center.
|
||||
- `severity`: `info` (default) | `warning` | `critical`.
|
||||
|
||||
**Empty-value semantics**: `toUserId` is required, but if it resolves to an **empty string** the node **silently skips** — status `succeeded`, `sentCount: 0`, output `{ sentCount: 0, invalidUserIds: [], toUserId: '', skipped: true, skipReason: 'toUserId is empty' }`, and `sendCommonNotify` is NOT called. This enables conditional-notify patterns: a script action that returns an empty recipient implements "notify only when X". An empty `message` still fails the node with `Missing required fields: message`.
|
||||
|
||||
**IMPORTANT**: this is a **system in-app notification** (bell badge + notification center + realtime socket push). It calls `NotificationService.sendCommonNotify` with `type=system` and **no email config — no email is sent**. Prefer it over `sendEmail` unless an actual email is required.
|
||||
|
||||
**Output:** `{ "sentCount": 1, "invalidUserIds": [], "toUserId": "usr..." }` (skipped case: see empty-value semantics above)
|
||||
|
||||
---
|
||||
|
||||
## 5. createRecord — Create records
|
||||
|
||||
```bash
|
||||
inkreach-teable action add <wfId> --type createRecord \
|
||||
--target-table tblXXX \
|
||||
--records '[{"fields":{"Name":"New","Source":"{{n0.record.id}}"}}]'
|
||||
```
|
||||
|
||||
**Config:**
|
||||
```json
|
||||
{
|
||||
"tableId": "tblXXX",
|
||||
"fieldKeyType": "id",
|
||||
"records": [ { "fields": { "fldName": "value" } } ]
|
||||
}
|
||||
```
|
||||
- `fieldKeyType`: `id` (default) or `name`.
|
||||
|
||||
**Output:** `{ "records": [ { "id": "recXXX", "fields": {...} } ] }`
|
||||
|
||||
---
|
||||
|
||||
## 6. 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": [ ... ] }`
|
||||
|
||||
---
|
||||
|
||||
## 7. 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 `{{nodeId.path}}` to inject upstream output. The positional `{{index.path}}` form is also accepted — both resolve identically:
|
||||
|
||||
| Reference | Resolves to |
|
||||
|-----------|-------------|
|
||||
| `{{n0.record.id}}` (≡ `{{1.record.id}}`) | trigger record id (trigger = node 1) |
|
||||
| `{{n0.record.fields.fldStatus}}` | trigger record's Status field |
|
||||
| `{{n2.result}}` (≡ `{{2.result}}`) | script action's return value |
|
||||
| `{{n3.records}}` (≡ `{{3.records}}`) | getRecords output array |
|
||||
| `{{n2.statusCode}}` (≡ `{{2.statusCode}}`) | httpRequest status |
|
||||
|
||||
- **`{{nodeId.path}}`** — node ID form (teable generates ids like `n0`, `n1`, `n2`…). Preferred for clarity.
|
||||
- **`{{index.path}}`** — numeric form; **index = topological execution order** (trigger is always 1, then actions in chain order).
|
||||
|
||||
**Empty resolution**: when a referenced value is missing/undefined, the variable resolves to `''` (see sendNotification §4 for skip semantics).
|
||||
|
||||
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,430 @@
|
||||
# Field Types Reference
|
||||
|
||||
Complete reference of all Teable field types, aliases, option shapes, and smart inference rules.
|
||||
|
||||
> ✅ **Verified 2026-08**: aliases, string choices, compact `--fields` format, smart
|
||||
> inference, and name resolution (`foreignTableName`/`linkFieldName`/`lookupFieldName`)
|
||||
> are implemented in the CLI (`src/lib/field-normalize.ts`, repo root)
|
||||
> and covered by unit tests (`field-normalize.spec.ts`, 20 cases).
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
### Last Modified Time (tracked-field references)
|
||||
|
||||
A `lastModifiedTime` field can track **only specific fields** instead of all of them, so
|
||||
the timestamp resets when a referenced field changes but stays put on ordinary edits.
|
||||
Write the references inside the `expression` option:
|
||||
|
||||
```bash
|
||||
# Track by field name — resets only when 流转状态 or 流转角色 change
|
||||
inkreach-teable field create --table-id tblXXX --name "流转时间" --type lastModifiedTime \
|
||||
--options '{"expression":"LAST_MODIFIED_TIME([流转状态],[流转角色])"}'
|
||||
|
||||
# Track by field id (mixed syntax also allowed)
|
||||
inkreach-teable field create --table-id tblXXX --name "流转时间" --type lastModifiedTime \
|
||||
--options '{"expression":"LAST_MODIFIED_TIME({fldAbc123},{fldDef456})"}'
|
||||
```
|
||||
|
||||
| Reference syntax | Meaning |
|
||||
|---|---|
|
||||
| `LAST_MODIFIED_TIME()` | Track **all** fields (default) |
|
||||
| `LAST_MODIFIED_TIME([Name])` | Reference a field by its name |
|
||||
| `LAST_MODIFIED_TIME({fldId})` | Reference a field by its id |
|
||||
|
||||
**Key behaviors**:
|
||||
|
||||
1. The expression is parsed server-side into `trackedFieldIds` (the field's effective
|
||||
`options.trackedFieldIds`); the stored `expression` is normalized back to the canonical
|
||||
`LAST_MODIFIED_TIME()` form while `trackedFieldIds` carries the references.
|
||||
2. An unresolvable field **name** returns `400 Unknown field referenced in expression: <name>`;
|
||||
unresolvable `{fldId}` values are simply ignored (harmless — never match).
|
||||
3. You can also set `options.trackedFieldIds` directly (an array of field ids); an explicit
|
||||
`trackedFieldIds` wins over the expression when both are present.
|
||||
4. Works on create **and** convert (`PUT /api/table/:tableId/field/:fieldId/convert` with
|
||||
`{"type":"lastModifiedTime","options":{"expression":"..."}}`).
|
||||
|
||||
---
|
||||
|
||||
## 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,265 @@
|
||||
# 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.*` (post-update values), `oldFields.*` (pre-update values, field-ID keys), `user`, `tableId`, `changedFieldIds` (array of field IDs whose value changed).
|
||||
|
||||
**changedFieldIds / oldFields semantics** (delivered on both v1 and v2 API update paths):
|
||||
- `oldFields` = full pre-update field snapshot keyed by field ID; `changedFieldIds` = deep-equal diff of before/after snapshots.
|
||||
- ⚠️ **`oldFields` is TOP-LEVEL** (`triggerInput.oldFields.fldXXX`), a sibling of `record` — NOT nested inside `record`. Script access: `triggerInput.oldFields?.['fldXXX']`; variable reference: `{{n0.oldFields.fldXXX}}`.
|
||||
- `fieldIds` watch-filter matches against `changedFieldIds`.
|
||||
- On batch mode (`_batch`), each `records[]` entry carries its own `changedFieldIds` + `oldFields`.
|
||||
- ⚠️ **Write-path coverage** (verified 2026-08): browser grid cell edits DO fire this trigger — every grid editor commit funnels through `record.updateCell` → the REST `updateRecord` API (no ShareDB op submit exists in the frontend). Covered surfaces: single/batch record update (REST v1+v2), grid paste (small `paste-by-id`, large `paste-*-stream`, range `paste`), grid cell clear (`clear` / `clear-by-id`), record duplicate, and programmatic/automation writes (workflow actions call the REST API). Form submissions intentionally emit ONLY the `formSubmitted` event (independent channel). Record deletions emit no automation event (no delete trigger category exists).
|
||||
- Values resolve to `undefined` (not `[]`) if the emitting path could not snapshot before-values — scripts should guard `changedFieldIds ?? []`.
|
||||
|
||||
**Workflow-write echo suppression** (all 4 record triggers: `recordCreated` / `recordUpdated` / `recordCondition` / `recordCreatedOrUpdated`):
|
||||
- Writes made BY workflow executions (script `fetch` with `triggerInput._accessToken`, `createRecords`/`updateRecords` actions) are tagged `writeSource: {workflowId, executionId}` on the event.
|
||||
- **Self-echoes are always dropped**: a workflow never fires on its own writes (the script's PATCH re-triggering its own workflow was the duplication bug — one edit cascaded into 3-5 executions, every echo ending in a `skipped` no-op guard).
|
||||
- Writes by OTHER workflows still fire by default — cross-workflow chaining is load-bearing (e.g. the order-sync job 建档 and this workflow stamping the initial 待处理). Opted-in events carry `triggerInput.writeSource.workflowId` for loop-guarding inside scripts.
|
||||
- Config `"skipWorkflowWrites": true` additionally drops cross-workflow writes (stronger dedup when a workflow should react ONLY to human edits).
|
||||
- Human grid edits, form/API/CLI writes are never tagged and always fire; suppressed writes still reach other listeners (e.g. record subscriptions) — only record-trigger handlers drop them.
|
||||
|
||||
---
|
||||
|
||||
## 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.*`, `user`, `tableId`; on update events also top-level `oldFields.*` + `changedFieldIds` (see §2 semantics).
|
||||
|
||||
---
|
||||
|
||||
## 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; on update events also top-level `oldFields.*` + `changedFieldIds` (see §2 semantics).
|
||||
|
||||
---
|
||||
|
||||
## 5. formSubmitted — When a form is submitted
|
||||
|
||||
```bash
|
||||
inkreach-teable trigger set <wfId> --type formSubmitted --table-id tblXXX --form-ids 'viwXXX1,viwXXX2'
|
||||
```
|
||||
|
||||
**Config:**
|
||||
```json
|
||||
{ "tableId": "tblXXX", "formIds": ["viwXXX1", "viwXXX2"] }
|
||||
```
|
||||
- `formIds` is an array of **form view ids** (views of type `form`). The trigger
|
||||
fires when **any** of the listed forms is submitted.
|
||||
- **Legacy compatibility:** the older single-string `formId` key is still
|
||||
accepted (migrated to `[formId]` automatically), so existing workflows keep
|
||||
working.
|
||||
- An empty `formIds` array means the trigger never fires.
|
||||
|
||||
**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.workflow.id` points to this workflow.
|
||||
- Create it: `inkreach-teable field create --table-id tblXXX --name "Go" --type button --options '{"workflow":{"id":"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.name` (primary field), `record.fields.<buttonFieldId>` (**only the clicked button field's count, NOT all fields**), `fieldId`, `user`, `runId`, `name`, `tableId`, `workflowId`.
|
||||
|
||||
⚠️ **`record.fields` contains ONLY the button field itself** (e.g. `{fldXxx:{count:1}}`), not the record's other fields. To read other fields (e.g. an "订单号" text field), use the back-channel API inside a script:
|
||||
```js
|
||||
const rec = await fetch(`${triggerInput._apiBaseUrl}/api/record/${triggerInput.record.id}`, {
|
||||
headers: { Authorization: 'Bearer ' + triggerInput._accessToken }
|
||||
}).then(r => r.json());
|
||||
return { orderId: rec.fields.fldXxx };
|
||||
```
|
||||
`record.name` (primary field) and `record.id` ARE directly available.
|
||||
|
||||
⚠️ 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,33 @@
|
||||
{
|
||||
"skill_name": "inkreach-teable",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "button-click-automation",
|
||||
"prompt": "帮我在 order_logs 表里加一个按钮字段叫「催单」,点这个按钮的时候触发一个自动化,自动化里跑一段脚本,脚本内容是返回当前记录的订单号字段。然后激活它,点一下按钮测试看看执行结果。",
|
||||
"expected_output": "应创建 button 字段(options.workflow.id 指向新 workflow)、创建 workflow、设 buttonClick 触发器、加 script action(读取 triggerInput.record.fields)、激活、fire button-click 测试、runs list 看到 succeeded。关键:button options 用 workflow 对象结构而非 workflowId 字符串。",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "scheduled-digest",
|
||||
"prompt": "我想每天定时跑一次自动化,去查一下 order_logs 表里所有记录,把数量统计出来。帮我配一个定时自动化。",
|
||||
"expected_output": "应创建 workflow、设 scheduledTime 触发器(timing.type + interval,不配后端忽略的字段如 triggerTime/weekdays)、加 getRecords action、加 script action 统计、激活。关键:知道 scheduledTime 后端只认 type+interval。",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "record-condition-trigger",
|
||||
"prompt": "当 order_logs 表里某条记录的「结果」字段变成「失败」的时候,自动发一封邮件通知到 ops@xingchen.tech。帮我配这个自动化。",
|
||||
"expected_output": "应创建 workflow、设 recordCondition 触发器(用 conditions 而非 filter,category 是 recordCondition 而非 recordMatchesConditions)、加 sendEmail action、激活。关键:用正确的自研契约键名 conditions。",
|
||||
"files": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "debug-failed-run",
|
||||
"prompt": "我有个自动化 wfl5F8dQRDloo28w909,最近好像没正常跑,帮我看看是什么情况。",
|
||||
"expected_output": "应先 workflow get 查 isActive、再 runs list --status failed 查失败记录、runs get 看具体错误。关键:用排查流程而非盲目重新激活。",
|
||||
"files": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
# 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-ids <csv>` |
|
||||
| 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>` |
|
||||
| Send system notification (站内信) | `sendNotification` | `--to-user <usrId>`, `--title`, `--message`, `--severity`, `--path` |
|
||||
|
||||
Full config details: [action-types.md](../api-reference/action-types.md).
|
||||
|
||||
> ⚠️ `action update --config '<json>'` takes the **flat action config** (e.g. `{"code":"...","timeout":120000}`), NOT `{"config":{...}}`. A wrapped config deploys an empty script that "succeeds" in ~1ms with `result: {}` and breaks downstream `{{nodeId.result.*}}` references. After any `--config` deploy, verify with `workflow get --json` that `nodes[].config.code` is non-empty. Details: [action-types.md § script](../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>
|
||||
```
|
||||
|
||||
### `workflow test` vs real fire
|
||||
|
||||
`workflow test <id>` runs actions synchronously with **mock input** (a fake
|
||||
`{record:{id:'rec_test',fields:{}}, user:{...}}`). It does NOT fire the
|
||||
trigger or use the queue. Two caveats:
|
||||
|
||||
- **Upstream outputs are absent in test mode** — if a script reads
|
||||
`triggerInput.record.fields.<X>`, it gets `{}` because the mock record has
|
||||
no fields. Test only validates that the script *runs without throwing*, not
|
||||
that it produces correct data.
|
||||
- For data-dependent validation, do a **real fire** (button-click / webhook)
|
||||
or perform the real triggering action, then check `runs list`.
|
||||
|
||||
## Variable References (cross-node data)
|
||||
|
||||
An action can reference upstream nodes' output in string fields via `{{nodeId.path}}` (or the compatible `{{index.path}}` positional form):
|
||||
|
||||
- `{{n0.record.id}}` — trigger record's id (n0 = trigger node)
|
||||
- `{{n0.record.fields.Status}}` — trigger record's Status field
|
||||
- `{{2.result}}` — output of the first action (numeric index form; index = topological execution order, trigger is always 1)
|
||||
|
||||
Both forms resolve to the same value — `{{n0.record.id}}` ≡ `{{1.record.id}}`. Use `{{nodeId.path}}` in new workflows for clarity.
|
||||
|
||||
> **Empty-value semantics**: if a variable resolves to an empty string, the node sees the literal empty value. For `sendNotification`, an empty `toUserId` makes the node **silently skip** (status `succeeded`, `sentCount: 0`) instead of failing — so a script action that returns an empty recipient implements "notify only when X" conditional patterns. An empty `message` still fails with `Missing required fields: message`.
|
||||
|
||||
## 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 '{{n0.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,249 @@
|
||||
# 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:3003`) |
|
||||
| `--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 (get/update/visible-choices take <tableId> <fieldIdOrName> — id or display name, auto-resolved)
|
||||
inkreach-teable field list <tableId> [--with-options]
|
||||
# --with-options: OPTIONS summary column + per-choice visibility conditions for select fields
|
||||
inkreach-teable field get <tableId> <fieldIdOrName>
|
||||
# human mode renders select choices with their visibilityFilter conditions; --json for raw
|
||||
inkreach-teable field create --table-id <tid> --name <n> [--type <t|alias>] [--options '<json>'] \
|
||||
[--description <d>] [--lookup-options '<json>'] [--is-lookup]
|
||||
# --type optional: inferred from --options shape / --name when omitted
|
||||
inkreach-teable field update <tableId> <fieldIdOrName> [--name <n>] [--options '<json>'] # --options goes via convert API
|
||||
inkreach-teable field visible-choices <tableId> <recordId> <fieldIdOrName> # evaluate select-choice visibilityFilter
|
||||
inkreach-teable field delete <tableId> <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>
|
||||
|
||||
# view presets (view-scoped shared named filter/sort bundles, applied via toolbar)
|
||||
inkreach-teable preset list <tableId> [--view-id <vid>]
|
||||
inkreach-teable preset create --table-id <tid> --view-id <vid> --name <n> [--description <d>] \
|
||||
[--filter '<json>'] [--sort '<json>'] [--group '<json>'] [--options '<json>'] [--column-meta '<json>']
|
||||
inkreach-teable preset update <presetId> --table-id <tid> [--name <n>] [--description <d>] [--filter '<json>' ...]
|
||||
inkreach-teable preset delete <presetId> --table-id <tid>
|
||||
```
|
||||
|
||||
## 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-ids <csv>` |
|
||||
| `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>` |
|
||||
| `sendNotification` | `--to-user <usrId>`, `--title <s>`, `--message <s>`, `--severity <s>`, `--path <s>` |
|
||||
|
||||
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`).
|
||||
|
||||
## subscription
|
||||
|
||||
```bash
|
||||
inkreach-teable subscription list
|
||||
inkreach-teable subscription get <subscriptionId>
|
||||
inkreach-teable subscription create --name <n> --table-id <tid> [options]
|
||||
inkreach-teable subscription update <subscriptionId> [options]
|
||||
inkreach-teable subscription delete <subscriptionId>
|
||||
inkreach-teable subscription events <subscriptionId>
|
||||
inkreach-teable subscription send <subscriptionId>
|
||||
```
|
||||
|
||||
Record digest/instant email subscriptions. Options (`--filter`, `--send-mode`,
|
||||
`--schedule-*`, `--trigger-on-enter`, …) and semantics: [subscription.md](subscription.md).
|
||||
|
||||
## 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'
|
||||
```
|
||||
|
||||
## plugin
|
||||
|
||||
Manage plugins and developer whitelist.
|
||||
|
||||
```bash
|
||||
# List plugins registered in the base
|
||||
inkreach-teable plugin list
|
||||
|
||||
# Register a URL-served plugin (runtimeType=url; status published immediately)
|
||||
# positions: "base"|"panel"|"view"|"dashboard"|"contextMenu"
|
||||
inkreach-teable plugin register --url <url> --name <name> [--positions '["panel"]'] [--logo <url>] [--description <d>]
|
||||
# Bundle (upload) plugins use a different flow — see skill guides/tables-fields.md
|
||||
# or api docs: create kebab-id plugin (developer whitelist) → upload → admin publish.
|
||||
|
||||
# Developer whitelist (admin only)
|
||||
inkreach-teable plugin dev list
|
||||
inkreach-teable plugin dev allow <userIdOrEmail> # add (userId or email)
|
||||
inkreach-teable plugin dev revoke <userIdOrEmail> # remove
|
||||
|
||||
# Install / uninstall (creates a base-level mount node)
|
||||
inkreach-teable plugin install --plugin <pluginId> --name "My Plugin" --position base
|
||||
# ⚠️ For view/dashboard-position plugins, run `plugin config enable` BEFORE
|
||||
# install, or the install API returns 403 RESTRICTED_RESOURCE
|
||||
# 'Plugin is not enabled for this base'.
|
||||
inkreach-teable plugin installs # list installs in current base
|
||||
inkreach-teable plugin uninstall --install <installId>
|
||||
|
||||
# Enable/disable plugins
|
||||
inkreach-teable plugin config enable --plugin <pluginId>
|
||||
inkreach-teable plugin config disable --plugin <pluginId>
|
||||
|
||||
# CSP apiOrigins (external origins plugins may fetch)
|
||||
inkreach-teable plugin csp list
|
||||
inkreach-teable plugin csp set <pluginId> --origins '["http://192.168.x.x:8001"]'
|
||||
```
|
||||
|
||||
Developer whitelist is admin-only (requires `user.isAdmin=true`). Routes:
|
||||
`/api/plugin-developers` (GET/POST/DELETE).
|
||||
|
||||
## 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,170 @@
|
||||
# Development & Deployment
|
||||
|
||||
> This guide documents the **inkreach-teable application repo**
|
||||
> (`ssh://git@gitea.inkreach.cc:2222/inkreachhub/inkreach-teable.git`) — the
|
||||
> deployed instance this CLI operates on — not this CLI repo. Paths like
|
||||
> `apps/nestjs-backend/` refer to that repo's checkout.
|
||||
|
||||
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` | `:3003` | `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 `:3003` 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 `:3003` 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)
|
||||
|
||||
### Webpack cache may not rewrite `dist/index.js` after a workspace package rebuild
|
||||
After rebuilding a workspace package (e.g. `@teable/openapi` dist), `pnpm build` in
|
||||
`apps/nestjs-backend` can report "compiled successfully" while leaving `dist/index.js`
|
||||
untouched (node_modules paths are treated as immutable cache inputs). Symptom: backend
|
||||
behavior unchanged after a code change. Fix: `rm apps/nestjs-backend/dist/index.js`
|
||||
then rebuild, and verify with `grep '<your-new-string>' dist/index.js`.
|
||||
|
||||
### Workspace packages must be rebuilt BEFORE the backend (else stale dist ships)
|
||||
Distinct from the cache trap above: `apps/nestjs-backend` never compiles `@teable/*`
|
||||
sources — it resolves `@teable/core`, `@teable/v2-core`, `@teable/openapi` … via
|
||||
`node_modules` symlinks to `packages/*/dist`. `@teable/v2-core` in particular is a
|
||||
webpack EXTERNAL: the running backend reads `packages/v2/core/dist/index.cjs` directly
|
||||
at runtime, so a backend rebuild does not even copy it.
|
||||
|
||||
SILENT FAILURE MODE (hit 2026-08-18, select-defaultValue cascade fix): edit
|
||||
`packages/core/src/…` + `packages/v2/core/src/…` → only rebuild backend + restart →
|
||||
live API keeps the old behavior, no error anywhere. Correct order:
|
||||
|
||||
```bash
|
||||
pnpm --filter @teable/core build # tsc → packages/core/dist/**
|
||||
pnpm --filter @teable/v2-core build # tsdown → dist/index.cjs + dist/index.js
|
||||
cd apps/nestjs-backend && rm -f dist/index.js && pnpm build # bundle + externals
|
||||
# verify the new code actually shipped BEFORE restarting:
|
||||
grep '<new-string>' packages/core/dist/<path>.js
|
||||
grep '<new-string>' packages/v2/core/dist/index.cjs
|
||||
# then the best-path restart (:3003)
|
||||
```
|
||||
(Many packages at once: `pnpm -F './packages/**' build`, then backend build.)
|
||||
|
||||
### 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`.
|
||||
|
||||
### Script sandbox npm deps need npm on the service PATH
|
||||
Script actions CAN use npm packages — declare them via the CLI
|
||||
(`action add/update --dependencies '[{"name":"axios","version":"1.6.0"}]'`
|
||||
or shorthand `axios@1.6.0,dayjs`) and `require('axios')` works in-sandbox.
|
||||
The backend installs deps into a cache dir (`/tmp/teable-automation-deps/<hash>`)
|
||||
via `spawn('npm', ['install', ...])`, keyed by `name@version`.
|
||||
|
||||
**Gotcha:** systemd user services default to `PATH=/usr/local/bin:/usr/bin:...`
|
||||
which OMITS the nvm bin dir where `npm` lives → scripts with deps fail with
|
||||
`Dependency install failed: spawn npm ENOENT`. Fix: add
|
||||
`Environment=PATH=/root/.nvm/versions/node/vX/bin:/usr/local/sbin:...` to
|
||||
`teable-backend.service`, `daemon-reload`, restart. Verify:
|
||||
`inkreach-teable workflow test <wfId>` on a script with `require('dayjs')` →
|
||||
`succeeded`.
|
||||
|
||||
### 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 + 7 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=<ask-maintainer> 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=<ask-maintainer> 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=<ask-maintainer> 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,89 @@
|
||||
# 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:3003` | 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` from `.env` to point its websocket at the backend — verify it equals the backend port (3003 on this host), not the default 3001.
|
||||
- End users browse `:3002`. The backend `:3003` is the API only (`BACKEND_SKIP_NEXT_START=true` disables the embedded Next server).
|
||||
- ⚠️ Port map differs from upstream Teable docs (:3000) — on this host `:3000` is held by an **unrelated container** (`inkreach-web`). Always verify with `ss -tlnp` before assuming.
|
||||
|
||||
## Health Checks
|
||||
|
||||
```bash
|
||||
# Backend API up?
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:3003/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 ':3003|:3002|:5432|:6379'
|
||||
```
|
||||
|
||||
If `:3003` 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:3003 --token <PAT> --base-id bseXXX
|
||||
inkreach-teable auth status # verify
|
||||
```
|
||||
|
||||
## Reference Credentials (local dev)
|
||||
|
||||
> Never commit real secrets. The reference dev instance uses:
|
||||
> - ⚠️ The old admin account `hr_admin@xingchen.tech` **no longer exists** (verified 2026-08). Current users: `inkreach@inkreach.pod` (admin) — password: ask maintainer, browser login only.
|
||||
> - Throwaway accounts for testing can be created via `POST /api/auth/signup` (open on this instance).
|
||||
> - 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,49 @@
|
||||
# Record Subscriptions
|
||||
|
||||
Digest/instant email subscriptions over table records. Full CRUD via
|
||||
`subscription list/get/create/update/delete/events/send`.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
inkreach-teable subscription list
|
||||
inkreach-teable subscription get <subscriptionId>
|
||||
inkreach-teable subscription create --name "Daily escalations" --table-id tblXXX [options]
|
||||
inkreach-teable subscription update <subscriptionId> [options]
|
||||
inkreach-teable subscription delete <subscriptionId>
|
||||
inkreach-teable subscription events <subscriptionId>
|
||||
inkreach-teable subscription send <subscriptionId> # force a digest send now
|
||||
```
|
||||
|
||||
## Options (create/update)
|
||||
|
||||
| Option | Meaning |
|
||||
|--------|---------|
|
||||
| `--name` | Display name |
|
||||
| `--table-id` | Target table |
|
||||
| `--no-watch-created` / `--no-watch-updated` | Exclude create/update events |
|
||||
| `--field-ids '<json>'` | Restrict watched fields |
|
||||
| `--record-ids '<json>'` | Restrict watched records |
|
||||
| `--display-field-id <fld>` | Field shown in the digest row |
|
||||
| `--attach-csv` / `--no-attach-csv` | Attach matching records as CSV |
|
||||
| `--filter '<IFilter json>'` | Scope: only records matching this filter |
|
||||
| `--trigger-on-enter` | Notify only on the transition INTO the filter condition (requires `--filter`); default is state semantics (every matching change) |
|
||||
| `--send-mode digest\|instant` | Digest (batched on schedule) or instant email |
|
||||
| `--recipients '<a@x.com,b@y.com>'` | Email recipients |
|
||||
| `--no-notify-in-app` | Disable in-app notification |
|
||||
| `--schedule-type daily\|weekly\|hours\|minutes` | Digest schedule kind |
|
||||
| `--schedule-time HH:mm` | Daily/weekly fire time |
|
||||
| `--schedule-weekday 0-6` | Weekly day (0 = Sunday) |
|
||||
| `--schedule-interval n` | Interval for hours/minutes modes |
|
||||
| `--inactive` | Create/update in inactive state |
|
||||
|
||||
## Semantics & gotchas
|
||||
|
||||
- `--filter` uses the SAME IFilter shape as view filters. Date values:
|
||||
`{"mode":"exactDate","exactDate":"2026-08-21","timeZone":"Asia/Shanghai"}`.
|
||||
User values are id arrays (`"Me"` resolves to the subscription creator).
|
||||
- Filter fields must be **visible to the creator** (enforced server-side,
|
||||
fail-closed: unknown operator or deleted field never matches).
|
||||
- `--trigger-on-enter` implements "notify only when it starts matching"
|
||||
(e.g. status turns critical); the default state semantics notify on every
|
||||
matching change.
|
||||
@@ -0,0 +1,406 @@
|
||||
# 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. Types accept full values or aliases (`sel` → `singleSelect`); per-type option shapes and inference rules: [field-types.md](../api-reference/field-types.md).
|
||||
|
||||
**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` | no | Full type name or alias; **omitted → inferred** from `--options` shape or `--name` pattern (rules: [field-types.md § Smart Inference](../api-reference/field-types.md)) |
|
||||
| `--options` | depends on type | JSON object; select `choices` accept plain strings — see [field-types.md § Option Shapes](../api-reference/field-types.md) |
|
||||
| `--description` | no | Optional |
|
||||
|
||||
**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 <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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```json
|
||||
{"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)
|
||||
|
||||
```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 |
|
||||
|
||||
**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:
|
||||
> ```json
|
||||
> {"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
|
||||
|
||||
```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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```bash
|
||||
# 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 |
|
||||
@@ -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 + 7 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,87 @@
|
||||
#!/bin/bash
|
||||
# Install/upgrade the inkreach-teable CLI + agent skill from this standalone repo.
|
||||
# Run from this skill's directory (or anywhere; paths resolve from this file).
|
||||
# What it does:
|
||||
# 1. Builds the CLI (this repo's root IS the package).
|
||||
# 2. Links the `inkreach-teable` command globally.
|
||||
# 3. Installs the skill to ~/.zcode/skills/inkreach-teable (user scope —
|
||||
# available in every ZCode workspace, shadows same-named workspace skills).
|
||||
# Re-run this script after pulling changes to refresh both the command and the skill.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Resolve the repo root: skill lives at <root>/skills/inkreach-teable/scripts/
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
SKILL_DIR="$REPO_ROOT/skills/inkreach-teable"
|
||||
|
||||
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 repo layout
|
||||
if [ ! -f "$REPO_ROOT/package.json" ] || [ ! -d "$REPO_ROOT/src" ]; then
|
||||
echo "ERROR: CLI source not found at $REPO_ROOT"
|
||||
echo " Ensure the inkreach-teable-cli repo is checked out at $REPO_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build
|
||||
echo "Building CLI from $REPO_ROOT ..."
|
||||
cd "$REPO_ROOT"
|
||||
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.
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
cat > "$HOME/.local/bin/inkreach-teable" <<WRAPPER
|
||||
#!/bin/bash
|
||||
exec node "$REPO_ROOT/dist/bin.js" "\$@"
|
||||
WRAPPER
|
||||
chmod +x "$HOME/.local/bin/inkreach-teable"
|
||||
|
||||
# Install the skill at user scope so it works in ANY workspace, including
|
||||
# checkouts of the inkreach-teable application repo (which no longer bundles it).
|
||||
if [ -d "$SKILL_DIR" ]; then
|
||||
mkdir -p "$HOME/.zcode/skills"
|
||||
rm -rf "$HOME/.zcode/skills/inkreach-teable"
|
||||
cp -r "$SKILL_DIR" "$HOME/.zcode/skills/inkreach-teable"
|
||||
echo "✓ skill installed to ~/.zcode/skills/inkreach-teable (user scope)"
|
||||
fi
|
||||
|
||||
# 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 $REPO_ROOT/dist/bin.js <command>"
|
||||
fi
|
||||
Reference in New Issue
Block a user