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

Rename the skill directory and update all references (SKILL.md frontmatter
name, AGENTS.md, design doc, install.sh). Git tracks this as a rename so
history is preserved.
This commit is contained in:
yeuimu
2026-08-05 10:35:04 +08:00
parent 3810c33d69
commit d3790ab856
12 changed files with 2027 additions and 4 deletions
@@ -0,0 +1,246 @@
# Action Types Reference
All 6 actions of the inkreach-teable automation engine. Actions run in topological order after the trigger; each receives the merged upstream output.
An action node:
```json
{
"id": "node-2",
"type": "action",
"category": "<one of the 6 types below>",
"config": { ... },
"name": null
}
```
Use `inkreach-teable action add <wfId> --type <T> ...` (don't hand-build the JSON).
---
## Sandbox Globals (all script actions)
Inside a `script` action, these are available:
| Global | Description |
|--------|-------------|
| `triggerInput` | Merged output of trigger + all upstream actions. Contains `_accessToken`, `_apiBaseUrl`, `_baseId`, `_workflowId`, `_nodeId`, `_secrets`, `_publicOrigin` |
| `console` | Captured to execution logs (`logs.stdout` / `logs.stderr`) |
| `fetch` | Standard fetch (30s timeout recommended) |
| `process.env` | Secrets (by name) + `AUTOMATION_TOKEN` + `PUBLIC_ORIGIN` |
| `require` | Controlled — allows whitelisted npm deps declared via `dependencies` |
**NOT available**: `fs`, `eval`, `Function`, `process` (only `process.env`), `child_process`.
**Back-channel API access**: inside scripts, call the instance's own API:
```js
const res = await fetch(triggerInput._apiBaseUrl + '/api/table/tblXXX/record', {
headers: { Authorization: 'Bearer ' + triggerInput._accessToken }
});
```
`_accessToken` is a 1-hour JWT auto-created on activation (scope: table|read + record|read/create/update).
---
## 1. script — Run JavaScript
```bash
inkreach-teable action add <wfId> --type script \
--code 'return { doubled: triggerInput.record.fields.Amount * 2 }'
# or from a file
inkreach-teable action add <wfId> --type script --code-file ./my-script.js --timeout 120000
```
**Config:**
```json
{
"code": "// JS code",
"timeout": 60000,
"dependencies": [ { "name": "axios", "version": "1.6.0" } ]
}
```
- `timeout`: default 60000 (120000 in batch mode).
- `dependencies`: optional npm packages; `require('axios')` works in-sandbox.
**Output:**
```json
{ "result": "<return value>", "logs": { "stdout": "...", "stderr": "..." }, "executionTime": 123 }
```
In batch mode (`triggerInput.records` array):
```js
const records = triggerInput.records || [triggerInput.record];
const results = await Promise.allSettled(records.map(async (r) => {
// per-record work
}));
return { ok: results.filter(r => r.status === 'fulfilled').length };
```
---
## 2. sendEmail — Send an email
```bash
inkreach-teable action add <wfId> --type sendEmail \
--to user@example.com --subject 'Alert' --body '<p>Check this</p>'
```
**Config:**
```json
{
"to": "user@example.com",
"subject": "Alert",
"body": "<html>...</html>",
"transporterName": "automation"
}
```
- `transporterName`: `automation` (default) or `notify`. Requires SMTP configured on the server.
**Output:** `{ "sent": true, "to": "..." }`
Variable reference: `--body '{{1.record.fields.Name}}'` injects upstream data.
---
## 3. httpRequest — Call an external API
```bash
inkreach-teable action add <wfId> --type httpRequest \
--url https://api.example.com/webhook --method POST \
--headers '{"X-Api-Key":"xxx"}' --req-body '{"event":"test"}'
```
**Config:**
```json
{
"url": "https://...",
"method": "POST",
"headers": { "X-Api-Key": "xxx" },
"body": { ... }
}
```
- `method`: GET (default) | POST | PUT | PATCH | DELETE.
- 30s hard timeout.
- GET/HEAD do not send a body.
**Output:** `{ "statusCode": 200, "headers": {...}, "body": <parsed> }`
- Non-2xx still returns output but the node status is `failed`.
---
## 4. createRecord — Create records
```bash
inkreach-teable action add <wfId> --type createRecord \
--target-table tblXXX \
--records '[{"fields":{"Name":"New","Source":"{{1.record.id}}"}}]'
```
**Config:**
```json
{
"tableId": "tblXXX",
"fieldKeyType": "id",
"records": [ { "fields": { "fldName": "value" } } ]
}
```
- `fieldKeyType`: `id` (default) or `name`.
**Output:** `{ "records": [ { "id": "recXXX", "fields": {...} } ] }`
---
## 5. updateRecord — Update records
```bash
inkreach-teable action add <wfId> --type updateRecord \
--target-table tblXXX \
--records '[{"id":"recXXX","fields":{"Status":"Done"}}]'
```
**Config:**
```json
{
"tableId": "tblXXX",
"fieldKeyType": "id",
"records": [ { "id": "recXXX", "fields": { "fldStatus": "Done" } } ]
}
```
- Each record **must** include `id` (recordId).
**Output:** `{ "records": [ ... ] }`
---
## 6. getRecords — Query records
```bash
inkreach-teable action add <wfId> --type getRecords \
--target-table tblXXX \
--query '{"pageSize":100,"filter":{"conjunction":"and","filterSet":[]}}'
```
**Config:**
```json
{
"tableId": "tblXXX",
"query": {
"pageSize": 100,
"page": 1,
"filter": { "conjunction": "and", "filterSet": [...] },
"orderBy": "fldXXX",
"search": "keyword"
}
}
```
- `search` string is auto-normalized to the `search[]` tuple format the API expects.
**Output:** `{ "records": [...], "total": 123 }`
---
## Variable References
In any action's **string** config field, use `{{nodeIndex.path}}` to inject upstream output:
| Reference | Resolves to |
|-----------|-------------|
| `{{1.record.id}}` | trigger record id (trigger = node 1) |
| `{{1.record.fields.fldStatus}}` | trigger record's Status field |
| `{{2.result}}` | script action's return value (node 2) |
| `{{3.records}}` | getRecords output array (node 3) |
| `{{2.statusCode}}` | httpRequest status (node 2) |
**Node index** = topological execution order (trigger is always 1, then actions in chain order).
Script actions can also read everything via `triggerInput` (the merged upstream output):
```js
const prev = triggerInput; // has all upstream fields merged
```
## Common Action Patterns
### Call OpenAI with a secret
```js
// secret named OPENAI_KEY must be set first
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.OPENAI_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: triggerInput.record.fields.Prompt }]
})
});
const data = await res.json();
return { reply: data.choices[0].message.content };
```
### Conditional logic
```js
const status = triggerInput.record.fields.Status;
if (status === 'Done') {
// call API
return { skipped: false };
}
return { skipped: true };
```
(There's no dedicated "if" node — do branching inside a script.)
@@ -0,0 +1,389 @@
# Field Types Reference
Complete reference of all Teable field types, aliases, option shapes, and smart inference rules.
---
## Type Overview
| `--type` value | Display name | Purpose | Requires `--options` |
|---------------|-------------|---------|---------------------|
| `singleLineText` | Single Line Text | Short text (titles, names, IDs) | No |
| `longText` | Long Text | Rich text, descriptions, notes | No |
| `number` | Number | Integers or decimals | No (defaults to integer) |
| `singleSelect` | Single Select | Pick one from fixed options | Yes (`choices`) |
| `multipleSelect` | Multiple Select | Pick many (tags/labels) | Yes (`choices`) |
| `date` | Date | Date or datetime | No |
| `checkbox` | Checkbox | Yes/no toggle | No |
| `rating` | Rating | Star/numeric rating 1..10 | No (defaults: max=5, star, yellow) |
| `attachment` | Attachment | Files, images, documents | No |
| `user` | User | Person/collaborator reference | No |
| `link` | Link | Reference records in another table | Yes (`foreignTableId`/`foreignTableName`) |
| `lookup` | Lookup | Mirror a field from linked record | Yes (`lookupOptions`) |
| `rollup` | Rollup | Aggregate linked records (SUM, COUNT, etc.) | Yes (expression + `lookupOptions`) |
| `conditionalRollup` | Conditional Rollup | Aggregate from ANY table (no link required) | Yes (expression + `lookupOptions` + conditions) |
| `formula` | Formula | Per-row computation | Yes (`expression`) |
| `createdTime` | Created Time | Auto-set when record created | No |
| `lastModifiedTime` | Last Modified Time | Auto-updated on any change | No |
| `createdBy` | Created By | Auto-set to creator's user | No |
| `lastModifiedBy` | Last Modified By | Auto-updated to last editor | No |
| `autoNumber` | Auto Number | Auto-incrementing integer | No |
| `button` | Button | Click-to-trigger automation | Yes (`workflow`; see Button section) |
### Date vs DateTime
There is no separate `dateTime` type. Use `date` and control the time component via `--options`:
```bash
# Date-only
--options '{"formatting":{"date":"YYYY-MM-DD","time":"None","timeZone":"UTC"}}'
# DateTime (with time)
--options '{"formatting":{"date":"YYYY-MM-DD","time":"HH:mm","timeZone":"America/New_York"}}'
```
| `time` value | Behavior |
|-------------|----------|
| `"None"` | Date only (no time picker) |
| `"HH:mm"` | 24-hour time |
| `"hh:mm A"` | 12-hour time with AM/PM |
---
## Type Aliases
When using `field create --type <alias>` or `table create --fields` compact format:
| Alias | Full type |
|-------|-----------|
| `text`, `str` | `singleLineText` |
| `long` | `longText` |
| `num`, `int` | `number` |
| `check`, `bool` | `checkbox` |
| `sel`, `select` | `singleSelect` |
| `multi`, `tags` | `multipleSelect` |
| `date` | `date` |
| `rate`, `star` | `rating` |
| `user`, `person` | `user` |
| `file`, `attach` | `attachment` |
| `link`, `rel` | `link` |
| `rollup` | `rollup` |
| `condRollup` | `conditionalRollup` |
| `formula` | `formula` |
| `auto` | `autoNumber` |
| `created` | `createdTime` |
| `modified` | `lastModifiedTime` |
| `createdBy` | `createdBy` |
| `modifiedBy` | `lastModifiedBy` |
| `button` | `button` |
**`table create --fields` compact format** also supports inline select options:
| Format | Meaning |
|--------|---------|
| `"Name:text"` | Single line text field |
| `"Amount:num"` | Number field |
| `"Status:sel:Todo,Doing,Done"` | Single select with choices |
| `"Tags:mul:Bug,Feature,UI"` | Multi-select with choices |
| `"Done:check"` | Checkbox |
| `"Score:rate"` | Rating (defaults) |
---
## Option Shapes by Type
### SingleSelect / MultipleSelect
```bash
# Simple — strings only, colors auto-assigned
inkreach-teable field create --table-id tblXXX --name "Status" --type singleSelect \
--options '{"choices":["Todo","Doing","Done"]}'
# With explicit colors
inkreach-teable field create --table-id tblXXX --name "Priority" --type singleSelect \
--options '{"choices":[{"name":"High","color":"redBright"},{"name":"Low","color":"gray"}]}'
# With default value
inkreach-teable field create --table-id tblXXX --name "Status" --type singleSelect \
--options '{"choices":["Todo","Done"],"defaultValue":"Todo"}'
```
| Option key | Type | Required | Notes |
|-----------|------|----------|-------|
| `choices` | `string[]` or `{name,color}[]` | yes | Colors auto-assigned from palette if omitted |
| `defaultValue` | `string` (singleSelect) / `string[]` (multipleSelect) | no | Must match a choice name |
| `preventAutoNewOptions` | `boolean` | no | Disable adding choices from cell editing |
**Available colors**: `blue`, `cyan`, `gray`, `green`, `orange`, `pink`, `purple`, `red`, `teal`, `yellow` — each with `Light2`, `Light1`, `Bright`, (base), `Dark1` variants. When colors are auto-assigned, the system cycles through a built-in palette.
### Number
```bash
# Integer (default — no options needed)
inkreach-teable field create --table-id tblXXX --name "Count" --type number
# Decimal with 2 places
inkreach-teable field create --table-id tblXXX --name "Price" --type number \
--options '{"formatting":{"precision":2}}'
# With default value
inkreach-teable field create --table-id tblXXX --name "Quantity" --type number \
--options '{"defaultValue":1}'
```
| Option key | Type | Default | Notes |
|-----------|------|---------|-------|
| `formatting.precision` | `number` | `0` | Decimal places (0 = integer) |
| `formatting.type` | `"decimal"` / `"integer"` | `"decimal"` | Number type |
| `showAs` | object | — | Display as percentage, currency, etc. |
| `defaultValue` | `number` | — | Numeric default for new records |
### Rating
```bash
# Quick: default (max=5, star icon, yellow)
inkreach-teable field create --table-id tblXXX --name "Score" --type rating
# Custom
inkreach-teable field create --table-id tblXXX --name "Priority" --type rating \
--options '{"max":3,"icon":"flame","color":"redBright"}'
# Max 10
inkreach-teable field create --table-id tblXXX --name "NPS" --type rating \
--options '{"max":10,"icon":"star","color":"tealBright"}'
```
| Option key | Type | Default | Notes |
|-----------|------|---------|-------|
| `max` | `number` | `5` | 1–10 |
| `icon` | `"star"` / `"moon"` / `"sun"` / `"zap"` / `"flame"` / `"heart"` / `"apple"` / `"thumb-up"` | `"star"` | Icon to display |
| `color` | `"yellowBright"` / `"redBright"` / `"tealBright"` | `"yellowBright"` | Icon fill color |
### Date
```bash
# Date only (default)
inkreach-teable field create --table-id tblXXX --name "Due" --type date
# Date with time
inkreach-teable field create --table-id tblXXX --name "Appointment" --type date \
--options '{"formatting":{"date":"YYYY-MM-DD","time":"HH:mm","timeZone":"Asia/Shanghai"}}'
# Auto-fill with "now" for new records
inkreach-teable field create --table-id tblXXX --name "Created At" --type date \
--options '{"defaultValue":"now"}'
```
| Option key | Type | Default | Notes |
|-----------|------|---------|-------|
| `formatting.date` | `string` | `"YYYY-MM-DD"` | Preset: `M/D/YYYY`, `D/M/YYYY`, `YYYY/MM/DD`, `YYYY-MM-DD`, `YYYY-MM`, `MM-DD`, `YYYY`, `MM`, `DD` |
| `formatting.time` | `"None"` / `"HH:mm"` / `"hh:mm A"` | `"None"` | Time picker if not `"None"` |
| `formatting.timeZone` | `string` | System local | IANA timezone name |
| `defaultValue` | `"now"` | — | Auto-fills current time on new records |
### Link
```bash
# Using table name (preferred)
inkreach-teable field create --table-id tblXXX --name "Project" --type link \
--options '{"foreignTableName":"Projects","relationship":"manyOne"}'
# Using table ID
inkreach-teable field create --table-id tblXXX --name "Project" --type link \
--options '{"foreignTableId":"tblXXX","relationship":"manyOne"}'
# Many-to-many
inkreach-teable field create --table-id tblXXX --name "Tags" --type link \
--options '{"foreignTableName":"Tags","relationship":"manyMany"}'
```
| Option key | Type | Required | Notes |
|-----------|------|----------|-------|
| `foreignTableName` | `string` | * | Resolved to `foreignTableId` by CLI; preferred over ID |
| `foreignTableId` | `string` | * | Direct ID; use `foreignTableName` instead |
| `relationship` | `"oneOne"` / `"oneMany"` / `"manyOne"` / `"manyMany"` | yes | Direction is from THIS table to the FOREIGN table |
| `isOneWay` | `boolean` | no | No symmetric field in foreign table |
| `filterByViewId` | `string` | no | Limit selectable records to a view |
| `visibleFieldIds` | `string[]` | no | Fields shown in linked record picker |
**Relationship quick reference**:
| Relationship | Meaning | Example |
|-------------|---------|---------|
| `manyOne` | Many records here → one record there | Tasks → Project |
| `oneMany` | One record here → many records there | Project → Tasks |
| `oneOne` | One-to-one | User ↔ Profile |
| `manyMany` | Many-to-many | Tasks ↔ Tags |
> **Name resolution**: use `foreignTableName` instead of `foreignTableId` — the CLI resolves it. Preferred over manual ID lookups.
### Formula
```bash
inkreach-teable field create --table-id tblXXX --name "Full Name" --type formula \
--options '{"expression":"{fldFirstName} & \" \" & {fldLastName}"}'
inkreach-teable field create --table-id tblXXX --name "Total" --type formula \
--options '{"expression":"{fldQuantity} * {fldPrice}"}'
```
| Option key | Type | Required | Notes |
|-----------|------|----------|-------|
| `expression` | `string` | yes | Formula referencing fields by ID: `{fldXXX}` |
| `timeZone` | `string` | no | For date functions |
| `formatting` | object | no | Formatting for result (number/date) |
**Field references**: use `{fldXXX}` with field IDs. Get IDs from `field list <tableId>`.
### Rollup
```bash
# Requires an existing link field + a target field in the linked table
inkreach-teable field create --table-id tblXXX --name "Total Hours" --type rollup \
--options '{"expression":"sum({values})"}' \
--lookup-options '{"linkFieldName":"Tasks","lookupFieldName":"Hours","foreignTableId":"tblTasks"}'
```
| Required | Meaning |
|----------|---------|
| `--options.expression` | Aggregation: `sum({values})`, `count({values})`, `average({values})`, `max({values})`, `min({values})`, `concatenate({values})`, `unique({values})`, `countDistinct({values})` |
| `--lookup-options.linkFieldName` | The link field in THIS table pointing to the foreign table |
| `--lookup-options.lookupFieldName` | The field in the FOREIGN table to aggregate |
| `--lookup-options.foreignTableId` | The foreign table (typically same as the link field's target) |
**Name resolution**: `linkFieldName` and `lookupFieldName` accept field display names — CLI resolves them to IDs.
### Lookup
```bash
# Mirror a field from a linked record
inkreach-teable field create --table-id tblXXX --name "Project Name" --type singleLineText \
--is-lookup \
--lookup-options '{"linkFieldName":"Project","lookupFieldName":"Name","foreignTableId":"tblProjects"}'
```
Lookup uses the base type of the field being mirrored (here copied as `singleLineText`), with `--is-lookup` and `--lookup-options`.
### ConditionalRollup
```bash
# Aggregate from ANY table without a link field
inkreach-teable field create --table-id tblXXX --name "Active Tasks" --type conditionalRollup \
--options '{"expression":"sum({values})"}' \
--lookup-options '{"lookupFieldName":"Estimate","foreignTableId":"tblTasks"}' \
--conditions '{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"isNot","value":"Done"}]}'
```
### Button
```bash
# Create a button field bound to a workflow
inkreach-teable field create --table-id tblXXX --name "Approve" --type button \
--options '{"label":"Approve","color":"blue","workflow":{"id":"wflXXX","name":"Approval Flow"}}'
# With click limit
inkreach-teable field create --table-id tblXXX --name "Run Once" --type button \
--options '{"label":"Run","color":"teal","maxCount":1,"workflow":{"id":"wflXXX"}}'
# With confirmation dialog
inkreach-teable field create --table-id tblXXX --name "Delete" --type button \
--options '{"label":"Delete","color":"red","workflow":{"id":"wflXXX"},"confirm":{"title":"Are you sure?","description":"This cannot be undone","confirmText":"Yes, delete"}}'
```
| Option key | Type | Required | Default | Notes |
|-----------|------|----------|---------|-------|
| `label` | `string` | no | `"Button"` | Display text on the button |
| `color` | Color enum | no | `"teal"` | Button color |
| `workflow.id` | `string` | **yes** | — | Workflow ID triggered on click |
| `workflow.name` | `string` | no | — | Workflow display name (informational) |
| `workflow.isActive` | `boolean` | no | — | Captured at creation time; reflects whether workflow was active |
| `maxCount` | `number` | no | — | Max clicks per record; empty = unlimited |
| `resetCount` | `boolean` | no | — | Reset click count behavior |
| `confirm.title` | `string` | no | — | Confirmation dialog title |
| `confirm.description` | `string` | no | — | Confirmation dialog body |
| `confirm.confirmText` | `string` | no | — | Confirm button label |
**Available button colors**: `blueLight2`, `blueLight1`, `blueBright`, `blue`, `blueDark1`, `cyan`, `cyanBright`, `cyanDark1`, `gray`, `grayBright`, `grayDark1`, `green`, `greenBright`, `greenDark1`, `orange`, `orangeBright`, `orangeDark1`, `pink`, `pinkBright`, `pinkDark1`, `purple`, `purpleBright`, `purpleDark1`, `red`, `redBright`, `redDark1`, `teal`, `tealBright`, `tealDark1`, `yellow`, `yellowBright`, `yellowDark1`.
**Key behaviors**:
1. **Workflow binding**: button clicks fire the `buttonClick` trigger of the bound workflow. The workflow MUST be active (`inkreach-teable workflow activate wflXXX`) for the button to be clickable.
2. **`workflow.isActive` snapshot**: captured when the field is created. If the workflow is deactivated later, the field's `isActive` goes stale — the button won't be clickable. Resolve by updating the field options or re-activating the workflow.
3. **Billing**: in this self-hosted CE fork, button fields are **always available** — billing gate is lifted.
4. **Cell value**: read-only `{"count": N}` tracking click count. Automatically incremented by the backend.
5. **Click limit**: when `maxCount` is set and count reaches it, button becomes disabled.
---
## Smart Inference
### Type inference from `--name`
When `--type` is omitted, the CLI infers the type from the field name:
| Name pattern | Inferred type |
|-------------|---------------|
| `status`, `priority`, `category`, `type`, `stage`, `level` | `singleSelect` |
| `tag`, `label` | `multipleSelect` |
| `description`, `notes`, `comment`, `content`, `body` | `longText` |
| `count`, `amount`, `price`, `quantity`, `total`, `number`, `age` | `number` |
| `rating`, `score`, `stars` | `rating` |
| `is*`, `has*`, `active`, `enabled`, `done`, `completed` | `checkbox` |
| `assignee`, `owner`, `user` | `user` |
| `attachment`, `file`, `image`, `photo`, `document` | `attachment` |
| `email`, `url` | `singleLineText` |
| `created time`, `created` (without "by") | `createdTime` |
| `last modified`, `updated` (without "by") | `lastModifiedTime` |
| `created by`, `author` | `createdBy` |
| `modified by`, `updated by` | `lastModifiedBy` |
### Options inference
| If you pass | Type inferred | Notes |
|-------------|--------------|-------|
| `--options '{"choices":["A","B"]}'` (no `--type`) | `singleSelect` | Colors auto-assigned |
| `--options '{"max":5}'` (no `--type`) | `rating` | Icon `star`, color `yellowBright` defaults applied |
| `--options '{"foreignTableName":"X"}'` (no `--type`) | `link` | |
| `--options '{"expression":"..."}'` (no `--type`) | `formula` | Unless combined with `foreignTableId` + conditions → `conditionalRollup` |
| `--options '{"workflow":{"id":"wflXXX"}}'` (no `--type`) | `button` | |
| No `--type`, no `--options` | Depot on `--name` pattern | Falls back to `singleLineText` if no match |
### Options defaults
| Type | Defaults applied when omitted |
|------|------------------------------|
| `rating` | `{"max":5,"icon":"star","color":"yellowBright"}` |
| `number` | `{"formatting":{"precision":0,"type":"decimal"}}` |
| `date` | `{"formatting":{"date":"YYYY-MM-DD","time":"None","timeZone":"<system>"}}` |
| `button` | `{"label":"Button","color":"teal"}` |
| `singleSelect` / `multipleSelect` | Choices auto-colored if no `color` in choice objects |
---
## Typecast Reference
`--typecast` converts human-readable values to internal IDs on `record create`/`record update`.
| Field type | Without typecast | With typecast |
|-----------|-----------------|---------------|
| `user` | `{"id":"usrXXX","title":"Alice"}` | `"Alice"` or `"alice@example.com"` |
| `link` (single) | `{"id":"recXXX"}` or `[{"id":"recXXX"}]` | `"Record Display Name"` |
| `link` (multi) | `[{"id":"recA"},{"id":"recB"}]` | `["Record A","Record B"]` |
**When to use**: always use `--typecast` when creating/updating records with user or link fields — it eliminates manual ID lookups.
---
## Field Type Selection Guide
| Data shape | Use | Not |
|-----------|-----|-----|
| Fixed options (status, priority) | `singleSelect` | `singleLineText` (no color/filter) |
| Multiple tags per record | `multipleSelect` | `singleSelect` (only one value) |
| Numbers with decimals | `number` with `precision` | `singleLineText` (can't calculate) |
| 1–10 ratings | `rating` | `number` (no star UI) |
| Yes/no flags | `checkbox` | `number` 0/1 (worse UX) |
| Dates/times | `date` with time formatting | `singleLineText` (can't sort/filter) |
| Reference another table | `link` | `singleSelect` (hardcoded list; breaks when added) |
| Auto-increment IDs | `autoNumber` | Manual numbering |
| Trigger an action | `button` | Any other type |
@@ -0,0 +1,236 @@
# Trigger Types Reference
All 8 triggers of the inkreach-teable automation engine. **Use these exact config keys** — they differ from official teable and from the (stale) `docs/automation/triggers.md`.
Every trigger node has this shape (stored in `workflow_node`):
```json
{
"id": "node-1",
"type": "trigger",
"category": "<one of the 8 types below>",
"config": { ... },
"name": null
}
```
> You don't build this JSON by hand — use `inkreach-teable trigger set <wfId> --type <T> ...` which constructs it correctly.
---
## 1. recordCreated — When a record is created
```bash
inkreach-teable trigger set <wfId> --type recordCreated --table-id tblXXX \
[--conditions '<filter json>'] \
[--batch '{"enabled":true,"batchWindow":2000,"batchSize":100}']
```
**Config:**
```json
{
"tableId": "tblXXX",
"conditions": null,
"batch": { "enabled": false, "batchWindow": 2000, "batchSize": 100 }
}
```
**Output variables** (available to downstream actions):
- `record.id`, `record.fields.<fieldId>`
- `user` (who created)
- `tableId`
---
## 2. recordUpdated — When a record is updated
```bash
inkreach-teable trigger set <wfId> --type recordUpdated --table-id tblXXX \
--field-ids '["fldXXX"]' \
[--batch '{...}']
```
**Config:**
```json
{
"tableId": "tblXXX",
"fieldIds": ["fldXXX"],
"batch": { ... }
}
```
- `fieldIds: []` (empty) = watch **all** fields (including future-created ones).
- ⚠️ **Key is `fieldIds`, NOT `watchFieldIds`.**
- ⚠️ recordUpdated **ignores `conditions`** — use `recordCondition` if you need filtering.
**Output:** `record.id`, `record.fields.*`, `record.oldFields.*` (pre-update values), `user`, `tableId`, `changedFieldIds`.
---
## 3. recordCondition — When a record matches conditions
```bash
inkreach-teable trigger set <wfId> --type recordCondition --table-id tblXXX \
--conditions '{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"is","value":"Done"}]}'
```
**Config:**
```json
{
"tableId": "tblXXX",
"conditions": { "conjunction": "and", "filterSet": [ ... ] },
"batch": { ... }
}
```
- ⚠️ **Category is `recordCondition`, NOT `recordMatchesConditions`.**
- `conditions` is **required**.
- Fires on create AND update when the record transitions to matching.
**conditions structure** (same as view filters):
```json
{
"conjunction": "and" | "or",
"filterSet": [
{ "fieldId": "fldXXX", "operator": "is", "value": "Done" },
{ "fieldId": "fldNum", "operator": "isGreater", "value": 100 }
]
}
```
Common operators: `is`, `isNot`, `isEmpty`, `isNotEmpty`, `isAnyOf`, `isNoneOf`, `contains`, `isGreater`, `isLess`, `isAfter`, `isBefore`.
**Output:** `record.id`, `record.fields.*`, `record.oldFields.*`, `user`, `tableId`.
---
## 4. recordCreatedOrUpdated — When a record is created or updated
```bash
inkreach-teable trigger set <wfId> --type recordCreatedOrUpdated --table-id tblXXX \
[--conditions '<filter json>'] \
[--batch '{...}']
```
**Config:**
```json
{ "tableId": "tblXXX", "conditions": null, "batch": { ... } }
```
- Ignores `fieldIds` (watches all fields).
- `conditions` optional.
**Output:** same as recordCreated + `record.oldFields.*` on updates.
---
## 5. formSubmitted — When a form is submitted
```bash
inkreach-teable trigger set <wfId> --type formSubmitted --table-id tblXXX --form-id viwXXX
```
**Config:**
```json
{ "tableId": "tblXXX", "formId": "viwXXX" }
```
- `formId` is the **form view's id** (a view of type `form`).
**Output:** `record.id`, `record.fields.*`, `user`, `tableId`, `formId`.
---
## 6. scheduledTime — On a schedule
```bash
inkreach-teable trigger set <wfId> --type scheduledTime \
--schedule '{"timing":{"type":"minutes","interval":5}}'
```
**Config (only these fields are honored):**
```json
{ "timing": { "type": "minutes", "interval": 5 } }
```
| `timing.type` | `interval` range | Meaning |
|---------------|------------------|---------|
| `minutes` | 1–60 | Every N minutes |
| `hours` | 1–24 | Every N hours |
| `days` | 1–N | Every N days |
| `months` | 1–N | Every N months (uses a fixed day) |
⚠️ **Backend limitations (known gotchas):**
- `triggerTime`, `weekdays`, `daysOfMonth`, `starting`, `ending`, `tz` are **all IGNORED**.
- `weeks` type is hardcoded to every Sunday (ignores `weekdays`).
- `OneTime` type is unsupported → falls back to every 60s.
- The scheduler is in-process `setInterval`; **multi-instance deployments fire duplicates**.
If you need precise scheduling, set `interval` to a friendly value. Do NOT pass the extra fields — they give false confidence.
**Output:** none useful (no record context); typically used with a `getRecords` + `script` action chain.
---
## 7. buttonClick — When a button field is clicked
```bash
inkreach-teable trigger set <wfId> --type buttonClick --table-id tblXXX --field-ids '["fldGo"]'
```
**Config:**
```json
{ "tableId": "tblXXX", "fieldIds": ["fldGo"] }
```
- The field **must be a Button-type field** whose `options.workflowId` points to this workflow.
- Create it: `inkreach-teable field create --table-id tblXXX --name "Go" --type button --options '{"workflowId":"wflXXX","label":"Go"}'`
**Triggering:** the click comes from `POST /api/table/{tableId}/record/{recordId}/{fieldId}/button-click` (the web UI calls this; CLI tests via `fire button-click`).
**Output:** `record.id`, `record.fields.*`, `fieldId`, `user`, `runId`, `name`, `tableId`, `workflowId`.
⚠️ The Button field's `options` captures a stale `isActive` snapshot at creation. After deactivating, the API validates the DB `isActive` instead — so a deactivated workflow returns 400 "not active" on click. Re-`activate` to fix.
---
## 8. webhook — On external HTTP POST
```bash
inkreach-teable trigger set <wfId> --type webhook
```
**Config:**
```json
{ "authorization": { "type": "none" } }
```
⚠️ **`authorization` is UI-only — the backend does NOT validate it.** Anyone with the URL can trigger it. Don't rely on it for security.
**Real webhook URL:**
```
POST http://<endpoint>/api/webhook/{baseId}/{workflowId}
```
- ⚠️ The route is `/api/webhook/{baseId}/{workflowId}` — **no** `/base/` or `/workflow/` segments (the frontend UI shows a different, WRONG URL).
- Test: `inkreach-teable fire webhook <baseId> <wfId> --body '{"any":"json"}'`
**Output:** `headers` (request headers), `body` (parsed JSON body), `method`, `baseId`, `webhookId`.
---
## Batch Processing
For `recordCreated`/`recordUpdated`/`recordCondition`/`recordCreatedOrUpdated`, add `--batch`:
```json
{ "enabled": true, "batchWindow": 2000, "batchSize": 100 }
```
- Events within `batchWindow` ms (up to `batchSize`) are merged into ONE run.
- The script receives `triggerInput.records` (array) instead of `triggerInput.record` (single).
- Use `Promise.allSettled` in scripts to handle the array concurrently.
```bash
inkreach-teable trigger set wflXXX --type recordCreated --table-id tblXXX \
--batch '{"enabled":true,"batchWindow":2000,"batchSize":100}'
inkreach-teable action add wflXXX --type script --code '
const records = triggerInput.records || [triggerInput.record];
const results = await Promise.allSettled(records.map(async (r) => {
// process each record
}));
return { processed: results.length };
'
```