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:
@@ -0,0 +1,191 @@
|
||||
# Automation Guide
|
||||
|
||||
Automations (workflows) are event-driven: a **trigger** fires, then **actions** run in topological order. inkreach-teable's automation engine is self-built — use the `inkreach-teable` CLI (not official `teable`).
|
||||
|
||||
> **Required reading before configuring triggers**: [trigger-types.md](../api-reference/trigger-types.md)
|
||||
> **Required reading before writing script actions**: [action-types.md](../api-reference/action-types.md)
|
||||
|
||||
## Mental Model
|
||||
|
||||
```
|
||||
[Trigger] → [Action 1] → [Action 2] → ... → [triggerEnd]
|
||||
```
|
||||
|
||||
- A workflow has **exactly one trigger node** (type `trigger`) and N action nodes (type `action`).
|
||||
- Nodes connect via **edges** (linear chain; each node has at most one upstream — no parallel branches).
|
||||
- **Activating** freezes a snapshot; execution runs against the snapshot, not the draft.
|
||||
- `hasDraft: true` = unactivated changes exist; re-`activate` to apply.
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `workflow list` | List all workflows in the base |
|
||||
| `workflow get <id>` | Detail (nodes + edges + config) |
|
||||
| `workflow create --name <n>` | Create (auto-adds a recordCreated trigger) |
|
||||
| `workflow update <id> [--name] [--description]` | Rename |
|
||||
| `workflow delete <id>` | Soft-delete |
|
||||
| `workflow activate <id>` | Snapshot + register trigger + create auto-token |
|
||||
| `workflow deactivate <id>` | Unregister trigger |
|
||||
| `workflow test <id>` | Mock-input test run (no trigger, no queue) |
|
||||
| `trigger set <id> --type <T> ...` | Set trigger type + config |
|
||||
| `action add <id> --type <T> ...` | Add an action node |
|
||||
| `action update <id> <nodeId> ...` | Update an action node |
|
||||
| `action delete <id> <nodeId>` | Remove an action node |
|
||||
| `secret set/list/delete` | Manage script secrets |
|
||||
| `runs list/get/rerun` | Execution history |
|
||||
| `fire button-click/webhook` | Manually trigger for testing |
|
||||
|
||||
## Trigger Selection
|
||||
|
||||
| User scenario | `--type` | Key options |
|
||||
|--------------|----------|-------------|
|
||||
| React to new records | `recordCreated` | `--table-id` |
|
||||
| React to record changes | `recordUpdated` | `--table-id`, `--field-ids` (empty = all) |
|
||||
| React when a condition matches | `recordCondition` | `--table-id`, `--conditions` (required) |
|
||||
| React to create OR update | `recordCreatedOrUpdated` | `--table-id`, `--conditions` (optional) |
|
||||
| Form submission | `formSubmitted` | `--table-id`, `--form-id` |
|
||||
| Schedule (only type+interval work) | `scheduledTime` | `--schedule '{"timing":{"type":"minutes","interval":5}}'` |
|
||||
| Button field click | `buttonClick` | `--table-id`, `--field-ids` |
|
||||
| External HTTP POST | `webhook` | (none; auth backend-ignored) |
|
||||
|
||||
**Batch processing** (record triggers only): add `--batch '{"enabled":true,"batchWindow":2000,"batchSize":100}'` to merge events within a window into a single run. The script receives an array of records.
|
||||
|
||||
Full config details: [trigger-types.md](../api-reference/trigger-types.md).
|
||||
|
||||
## Action Selection
|
||||
|
||||
| Goal | `--type` | Key options |
|
||||
|------|----------|-------------|
|
||||
| Run JS code (transform, call APIs, compute) | `script` | `--code '<js>'` or `--code-file <path>`, `--timeout <ms>` |
|
||||
| Send email | `sendEmail` | `--to`, `--subject`, `--body <html>` |
|
||||
| Call external HTTP | `httpRequest` | `--url`, `--method`, `--headers <json>`, `--req-body <json>` |
|
||||
| Create records | `createRecord` | `--target-table <id>`, `--records <json>` |
|
||||
| Update records | `updateRecord` | `--target-table <id>`, `--records <json>` |
|
||||
| Query records | `getRecords` | `--target-table <id>`, `--query <json>` |
|
||||
|
||||
Full config details: [action-types.md](../api-reference/action-types.md).
|
||||
|
||||
## Creation Workflow
|
||||
|
||||
```bash
|
||||
# 1. Gather IDs
|
||||
inkreach-teable table get tblXXX # confirm table
|
||||
inkreach-teable field list tblXXX # find field ids for triggers
|
||||
|
||||
# 2. Create workflow
|
||||
inkreach-teable workflow create --name "My automation"
|
||||
# → returns wflXXX
|
||||
|
||||
# 3. Set trigger
|
||||
inkreach-teable trigger set wflXXX --type recordUpdated \
|
||||
--table-id tblXXX --field-ids '["fldStatus"]'
|
||||
|
||||
# 4. Add action(s)
|
||||
inkreach-teable action add wflXXX --type script \
|
||||
--code 'return { status: triggerInput.record.fields.Status }'
|
||||
|
||||
# 5. (Optional) add secrets the script needs
|
||||
inkreach-teable secret set wflXXX --name API_KEY --value sk_xxx
|
||||
|
||||
# 6. Activate
|
||||
inkreach-teable workflow activate wflXXX
|
||||
# → returns snapshotId
|
||||
|
||||
# 7. Test
|
||||
inkreach-teable fire button-click tblXXX recXXX fldXXX # for buttonClick
|
||||
# or just perform the triggering action (create/update a record, POST webhook...)
|
||||
inkreach-teable runs list wflXXX
|
||||
```
|
||||
|
||||
## Editing an Existing Workflow
|
||||
|
||||
```bash
|
||||
# Change trigger config
|
||||
inkreach-teable trigger set wflXXX --type buttonClick --table-id tblXXX --field-ids '["fldNew"]'
|
||||
# Add another action
|
||||
inkreach-teable action add wflXXX --type sendEmail --to a@b.com --subject Done --body '<p>ok</p>'
|
||||
# Remove an action
|
||||
inkreach-teable action delete wflXXX node-12345
|
||||
# RE-ACTIVATE to snapshot the changes
|
||||
inkreach-teable workflow activate wflXXX
|
||||
```
|
||||
|
||||
`trigger set` / `action add/update/delete` internally do GET→modify→PUT the whole graph, so they're safe. But you **must re-activate** for changes to take effect at runtime.
|
||||
|
||||
## Secrets
|
||||
|
||||
Scripts often need API keys. Never hardcode — use secrets.
|
||||
|
||||
```bash
|
||||
inkreach-teable secret set wflXXX --name OPENAI_KEY --value sk-xxx --description "for AI calls"
|
||||
inkreach-teable secret list wflXXX # names only — value never returned
|
||||
inkreach-teable secret update wflXXX secXXX --value sk-new
|
||||
inkreach-teable secret delete wflXXX secXXX
|
||||
```
|
||||
|
||||
In a script action, read via `process.env.OPENAI_KEY`. Secrets are AES-encrypted at rest.
|
||||
|
||||
## Testing / Triggering
|
||||
|
||||
Most triggers fire from real events (record create/update, form submit, schedule). Two triggers are manually fireable for testing:
|
||||
|
||||
```bash
|
||||
# Button field click
|
||||
inkreach-teable fire button-click <tableId> <recordId> <fieldId>
|
||||
|
||||
# Webhook (no auth needed)
|
||||
inkreach-teable fire webhook <baseId> <workflowId> --body '{"event":"test"}'
|
||||
```
|
||||
|
||||
For other triggers, test by performing the real action (create/update a record, submit the form). Then inspect:
|
||||
|
||||
```bash
|
||||
inkreach-teable runs list wflXXX --status failed
|
||||
inkreach-teable runs get wflXXX <executionId>
|
||||
# rerun a failed execution
|
||||
inkreach-teable runs rerun wflXXX <executionId>
|
||||
```
|
||||
|
||||
## Variable References (cross-node data)
|
||||
|
||||
An action can reference upstream nodes' output in string fields via `{{nodeIndex.path}}`:
|
||||
|
||||
- `{{1.record.id}}` — trigger record's id (trigger = node 1)
|
||||
- `{{1.record.fields.Status}}` — trigger record's Status field
|
||||
- `{{2.records}}` — output of the first action (node 2)
|
||||
|
||||
Node index follows **topological execution order** (trigger is always 1). Script actions can also read the merged upstream output directly via `triggerInput`.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Notify on status change
|
||||
```bash
|
||||
inkreach-teable workflow create --name "Status→email"
|
||||
inkreach-teable trigger set wflXXX --type recordUpdated --table-id tblXXX --field-ids '["fldStatus"]'
|
||||
inkreach-teable action add wflXXX --type sendEmail \
|
||||
--to ops@x.com --subject 'Status changed' \
|
||||
--body '{{1.record.fields.Status}}'
|
||||
inkreach-teable workflow activate wflXXX
|
||||
```
|
||||
|
||||
### Daily digest
|
||||
```bash
|
||||
inkreach-teable trigger set wflXXX --type scheduledTime \
|
||||
--schedule '{"timing":{"type":"hours","interval":24}}'
|
||||
```
|
||||
|
||||
### Button → script → update another table
|
||||
```bash
|
||||
inkreach-teable trigger set wflXXX --type buttonClick --table-id tblA --field-ids '["fldGo"]'
|
||||
inkreach-teable action add wflXXX --type script --code '
|
||||
const r = triggerInput.record;
|
||||
// call back into the instance API using the auto-injected token
|
||||
const res = await fetch(triggerInput._apiBaseUrl + "/api/table/tblB/record", {
|
||||
method: "POST",
|
||||
headers: { Authorization: "Bearer " + triggerInput._accessToken, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ fields: { Source: r.id } })
|
||||
});
|
||||
return { created: await res.json() };
|
||||
'
|
||||
```
|
||||
@@ -0,0 +1,182 @@
|
||||
# CLI Reference
|
||||
|
||||
Complete command reference for `inkreach-teable`. Global options apply to all commands.
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--endpoint <url>` | Backend URL (default `http://localhost:3000`) |
|
||||
| `--token <pat>` | Personal Access Token |
|
||||
| `--base-id <id>` | Default base id |
|
||||
| `--json` | Output raw JSON (for agent parsing) |
|
||||
| `--debug` | Print HTTP requests/responses to stderr |
|
||||
| `-v, --version` | Show version |
|
||||
|
||||
Config precedence: global flag > env (`INKREACH_TEABLE_ENDPOINT`/`INKREACH_TEABLE_TOKEN`/`INKREACH_TEABLE_BASE_ID`) > `~/.inkreach-teable/config.json`.
|
||||
|
||||
## auth
|
||||
|
||||
```bash
|
||||
inkreach-teable auth status # verify token + show user
|
||||
inkreach-teable auth login --endpoint <url> --token <PAT> [--base-id <id>]
|
||||
inkreach-teable auth token list
|
||||
inkreach-teable auth token create --name <n> [--scopes <csv>] [--base-ids <csv>] [--full-access] [--expires <date>]
|
||||
inkreach-teable auth token delete <id>
|
||||
```
|
||||
|
||||
- `auth token create` returns the full token **only once**. Default scopes: `automation|read,automation|create,automation|update,automation|delete,table|read,record|read,record|create,record|update`.
|
||||
|
||||
## base / table / field / record / view
|
||||
|
||||
These resources use the official-teable-compatible API. Full guide: [tables-fields.md](tables-fields.md).
|
||||
|
||||
```bash
|
||||
# base
|
||||
inkreach-teable base list
|
||||
inkreach-teable base get <baseId>
|
||||
|
||||
# table
|
||||
inkreach-teable table list [--base-id <id>]
|
||||
inkreach-teable table get <tableId>
|
||||
inkreach-teable table create --name <n> [--base-id <id>] [--fields '<json>']
|
||||
inkreach-teable table update <tableId> --name <n>
|
||||
inkreach-teable table delete <tableId>
|
||||
|
||||
# field
|
||||
inkreach-teable field list <tableId>
|
||||
inkreach-teable field get <fieldId>
|
||||
inkreach-teable field create --table-id <tid> --name <n> --type <t> [--options '<json>']
|
||||
inkreach-teable field update <fieldId> [--name <n>] [--options '<json>']
|
||||
inkreach-teable field delete <fieldId>
|
||||
|
||||
# record
|
||||
inkreach-teable record get --table-id <tid> [--filter '<json>'] [--projection '<json>'] [--page-size <n>] [--page <n>] [--search <kw>]
|
||||
inkreach-teable record get-one <recordId>
|
||||
inkreach-teable record create --table-id <tid> --fields '<json>' # single
|
||||
inkreach-teable record create --table-id <tid> --records '<json>' # batch
|
||||
inkreach-teable record update --table-id <tid> --records '<json>'
|
||||
inkreach-teable record delete --table-id <tid> --record-ids <csv>
|
||||
|
||||
# view
|
||||
inkreach-teable view list <tableId>
|
||||
inkreach-teable view get <viewId>
|
||||
inkreach-teable view create --table-id <tid> --name <n> [--type <t>]
|
||||
inkreach-teable view update <viewId> [--name <n>]
|
||||
inkreach-teable view delete <viewId>
|
||||
```
|
||||
|
||||
## workflow
|
||||
|
||||
```bash
|
||||
inkreach-teable workflow list [--base-id <id>]
|
||||
inkreach-teable workflow get <workflowId> [--base-id <id>]
|
||||
inkreach-teable workflow create --name <n> [--description <d>] [--base-id <id>]
|
||||
inkreach-teable workflow update <workflowId> [--name <n>] [--description <d>] [--base-id <id>]
|
||||
inkreach-teable workflow delete <workflowId> [--base-id <id>]
|
||||
inkreach-teable workflow activate <workflowId> [--base-id <id>]
|
||||
inkreach-teable workflow deactivate <workflowId> [--base-id <id>]
|
||||
inkreach-teable workflow test <workflowId> [--base-id <id>]
|
||||
```
|
||||
|
||||
Full automation flow: [automation-guide.md](automation-guide.md).
|
||||
|
||||
## trigger
|
||||
|
||||
```bash
|
||||
inkreach-teable trigger set <workflowId> --type <T> [options] [--base-id <id>]
|
||||
```
|
||||
|
||||
| `--type` | Required options |
|
||||
|----------|------------------|
|
||||
| `recordCreated` | `--table-id` |
|
||||
| `recordUpdated` | `--table-id`, `--field-ids '<json>'` |
|
||||
| `recordCondition` | `--table-id`, `--conditions '<json>'` |
|
||||
| `recordCreatedOrUpdated` | `--table-id` |
|
||||
| `formSubmitted` | `--table-id`, `--form-id <id>` |
|
||||
| `scheduledTime` | `--schedule '{"timing":{"type":"minutes","interval":5}}'` |
|
||||
| `buttonClick` | `--table-id`, `--field-ids '<json>'` |
|
||||
| `webhook` | (none) |
|
||||
|
||||
Common optional: `--batch '<json>'` (record triggers only).
|
||||
|
||||
Details: [trigger-types.md](../api-reference/trigger-types.md).
|
||||
|
||||
## action
|
||||
|
||||
```bash
|
||||
inkreach-teable action add <workflowId> --type <T> [options] [--base-id <id>]
|
||||
inkreach-teable action update <workflowId> <nodeId> [--config '<json>'] [--code <js>] [--code-file <path>] [--name <n>]
|
||||
inkreach-teable action delete <workflowId> <nodeId> [--base-id <id>]
|
||||
```
|
||||
|
||||
Per-type options:
|
||||
|
||||
| `--type` | Options |
|
||||
|----------|---------|
|
||||
| `script` | `--code <js>` / `--code-file <path>`, `--timeout <ms>` |
|
||||
| `sendEmail` | `--to <email>`, `--subject <s>`, `--body <html>` |
|
||||
| `httpRequest` | `--url <u>`, `--method <m>`, `--headers <json>`, `--req-body <json>` |
|
||||
| `createRecord` | `--target-table <id>`, `--records <json>` |
|
||||
| `updateRecord` | `--target-table <id>`, `--records <json>` |
|
||||
| `getRecords` | `--target-table <id>`, `--query <json>` |
|
||||
|
||||
Details: [action-types.md](../api-reference/action-types.md).
|
||||
|
||||
## secret
|
||||
|
||||
```bash
|
||||
inkreach-teable secret list <workflowId> [--base-id <id>]
|
||||
inkreach-teable secret set <workflowId> --name <n> --value <v> [--description <d>] [--base-id <id>]
|
||||
inkreach-teable secret update <workflowId> <secretId> [--name <n>] [--value <v>] [--description <d>] [--base-id <id>]
|
||||
inkreach-teable secret delete <workflowId> <secretId> [--base-id <id>]
|
||||
```
|
||||
|
||||
Values are write-only — `list` never returns them. Scripts read via `process.env.<NAME>`.
|
||||
|
||||
## runs
|
||||
|
||||
```bash
|
||||
inkreach-teable runs list <workflowId> [--status failed|succeeded|running] [--base-id <id>]
|
||||
inkreach-teable runs get <workflowId> <executionId> [--base-id <id>]
|
||||
inkreach-teable runs rerun <workflowId> <executionId> [--base-id <id>]
|
||||
```
|
||||
|
||||
`rerun` only works on `failed` executions (backend validates).
|
||||
|
||||
## fire
|
||||
|
||||
```bash
|
||||
inkreach-teable fire button-click <tableId> <recordId> <fieldId>
|
||||
inkreach-teable fire webhook <baseId> <workflowId> [--body '<json>'] [--method <m>]
|
||||
```
|
||||
|
||||
Manually trigger for testing. Webhook needs no auth (backend is `@Public`).
|
||||
|
||||
## sql
|
||||
|
||||
```bash
|
||||
inkreach-teable sql '<SELECT ...>' [--base-id <id>]
|
||||
```
|
||||
|
||||
Read-only. Must use `dbTableName`/`dbFieldName` (from `table get`/`field get`), double-quote identifiers, add `LIMIT 100`.
|
||||
|
||||
```bash
|
||||
inkreach-teable sql 'SELECT "name" FROM "bseXXX"."tbl_abc" LIMIT 10'
|
||||
```
|
||||
|
||||
## Output Modes
|
||||
|
||||
- **Human (default)**: tables / key-values, colored where supported.
|
||||
- **`--json`**: compact raw JSON on stdout. Errors in JSON mode:
|
||||
```json
|
||||
{ "error": { "status": 404, "code": "NOT_FOUND", "message": "..." } }
|
||||
```
|
||||
(exit code 1)
|
||||
|
||||
## Error Troubleshooting
|
||||
|
||||
1. `inkreach-teable auth status` — token valid? endpoint reachable?
|
||||
2. Verify IDs: `table get`, `field get`, `workflow get` (typo'd IDs → 404).
|
||||
3. Check permissions: token scopes + base role. `403` → role too low.
|
||||
4. `--debug` to see the raw HTTP request/response.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Development & Deployment
|
||||
|
||||
How to build, restart, and debug this inkreach-teable instance. Covers the
|
||||
dual-process architecture, known gotchas, and test infrastructure. For daily
|
||||
operations (tables/automation), use the `inkreach-teable` CLI instead — this guide is
|
||||
for changing the source code itself.
|
||||
|
||||
## Architecture
|
||||
|
||||
Two **separate production processes** managed by systemd user services. Do NOT
|
||||
use `pnpm dev` as a long-running service — the embedded Next.js dev server
|
||||
inside NestJS is unstable and will hang or OOM-crash.
|
||||
|
||||
| Service | Command | Port | Mode |
|
||||
|---------|---------|------|------|
|
||||
| `teable-backend.service` | `node dist/index.js` | `:3000` | `NODE_ENV=production`, `BACKEND_SKIP_NEXT_START=true` |
|
||||
| `teable-frontend.service` | `next start -p 3002 -H 0.0.0.0` | `:3002` | `NODE_ENV=production` |
|
||||
|
||||
Users access `:3002`; the backend `:3000` serves the API only.
|
||||
|
||||
## Build & Restart
|
||||
|
||||
### Backend (no hot reload)
|
||||
After editing anything under `apps/nestjs-backend/src/`:
|
||||
```bash
|
||||
cd apps/nestjs-backend && pnpm build # webpack, ~18s
|
||||
systemctl --user restart teable-backend.service
|
||||
```
|
||||
`dist/` from `pnpm dev:swc` is hot-update fragments, NOT a valid artifact — always `nest build`.
|
||||
|
||||
### Frontend
|
||||
```bash
|
||||
pnpm -r -F './packages/**' build # build dependent packages first
|
||||
pnpm --filter @teable/app build-fast
|
||||
systemctl --user restart teable-frontend.service
|
||||
```
|
||||
Browser cache is aggressive — hard-refresh (Ctrl+Shift+R) after a rebuild.
|
||||
|
||||
### Recover from OOM crash
|
||||
If `:3000` is unreachable but systemd says `active`:
|
||||
```bash
|
||||
systemctl --user stop teable-backend.service
|
||||
pkill -9 -f 'dist/index.js'
|
||||
systemctl --user start teable-backend.service
|
||||
```
|
||||
|
||||
## Known Gotchas (from real debugging)
|
||||
|
||||
### Node v24 fetch drops the Authorization header
|
||||
The global `fetch()` on Node v24 (undici) drops the `Authorization` request
|
||||
header in certain cases (redirects, proxies). **Use `node:http`/`node:https`**
|
||||
directly for any backend API call, or the `axios`-style clients. This is why
|
||||
the `inkreach-teable` CLI uses `node:http` instead of `fetch`.
|
||||
|
||||
### Script sandbox has no `process` global
|
||||
In automation script actions, `typeof process === 'undefined'`. Access secrets
|
||||
via `process.env.NAME` (this is allowed — only `process.env` is exposed, not
|
||||
`process` itself). Use `triggerInput._secrets`, `triggerInput._accessToken`,
|
||||
`triggerInput._apiBaseUrl` for back-channel API calls. No `fs`, `eval`,
|
||||
`Function`, or `child_process`.
|
||||
|
||||
### scheduledTime backend is a simplified implementation
|
||||
The backend `scheduled-time.trigger.ts` only honors `timing.type` + `timing.interval`.
|
||||
It ignores `triggerTime`, `weekdays`, `daysOfMonth`, `starting`, `ending`, `tz`.
|
||||
`weeks` type is hardcoded to every Sunday. `OneTime` falls back to every 60s.
|
||||
The scheduler is in-process `setInterval` — multi-instance deployments fire
|
||||
duplicates. See [trigger-types.md § scheduledTime](../api-reference/trigger-types.md#6-scheduledtime--on-a-schedule).
|
||||
|
||||
### Webhook route mismatch
|
||||
Frontend UI shows `/api/webhook/base/{baseId}/workflow/{workflowId}` but the
|
||||
backend's real route is `/api/webhook/{baseId}/{workflowId}` (no `/base/` or
|
||||
`/workflow/` segments). The CLI uses the real route. Webhook `authorization`
|
||||
config is UI-only — backend does NOT validate it.
|
||||
|
||||
### Button field options capture stale isActive
|
||||
A Button field's `options.workflow.isActive` is a snapshot from creation time.
|
||||
After deactivating a workflow, the API validates the DB `is_active` instead —
|
||||
a click returns 400 "not active". Re-`activate` the workflow, or recreate the
|
||||
field. Always verify DB state, not field options.
|
||||
|
||||
### base_node pollution (sidebar perf)
|
||||
Workflows create entries in the `base_node` table, which feeds the sidebar
|
||||
navigation tree. Hundreds of test workflows → thousands of DOM nodes → jank.
|
||||
The sidebar is now virtualized (`@tanstack/react-virtual`), but avoid
|
||||
generating throwaway workflows; `deactivate` instead of `delete` to keep them
|
||||
queryable.
|
||||
|
||||
### Key-name differences from official teable
|
||||
The self-built automation uses `conditions` (not `filter`), `fieldIds` (not
|
||||
`watchFieldIds`), category `recordCondition` (not `recordMatchesConditions`).
|
||||
The `inkreach-teable` CLI handles these correctly; never use the official `teable`
|
||||
CLI for automations. See [SKILL.md § Key Constraints](../SKILL.md#3-key-constraints).
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
Automation E2E tests live in `scripts/automation-test/`:
|
||||
```bash
|
||||
node scripts/automation-test/run-all.js # 59 cases: 8 triggers + 6 actions + cross-features
|
||||
node scripts/stress-test/stress-test.js # concurrency stress test
|
||||
```
|
||||
Lib framework: `scripts/automation-test/lib.js`. Uses Node `http` (not fetch)
|
||||
due to the auth-header bug. Test workflows are **deactivated, not deleted**
|
||||
(retention policy) — the base has ~600 historical test workflows.
|
||||
|
||||
## Useful DB Queries
|
||||
|
||||
```bash
|
||||
# Count workflows by active state
|
||||
PGPASSWORD=teable psql -h localhost -U teable -d teable -c \
|
||||
"SELECT is_active, count(*) FROM workflow WHERE deleted_time IS NULL GROUP BY is_active;"
|
||||
|
||||
# Find button fields and their bound workflows
|
||||
PGPASSWORD=teable psql -h localhost -U teable -d teable -c \
|
||||
"SELECT id, name, options->'workflow'->>'id' AS wf_id, options->'workflow'->>'isActive' AS active
|
||||
FROM field WHERE type='button';"
|
||||
|
||||
# Recent executions
|
||||
PGPASSWORD=teable psql -h localhost -U teable -d teable -c \
|
||||
"SELECT id, workflow_id, status, started_at FROM workflow_execution
|
||||
ORDER BY started_at DESC LIMIT 10;"
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
# Instance Access
|
||||
|
||||
How to reach the deployed inkreach-teable instance, acquire credentials, and
|
||||
verify health. This file describes the **reference deployment** — adapt
|
||||
endpoint/ports for your own deployment.
|
||||
|
||||
## Connection
|
||||
|
||||
| Service | URL / Port | Mode | Purpose |
|
||||
|---------|-----------|------|---------|
|
||||
| Backend API | `http://localhost:3000` | production (`NODE_ENV=production`) | REST API, automation engine, websocket |
|
||||
| Frontend | `http://localhost:3002` | production | End-user web UI (Next.js) |
|
||||
| PostgreSQL | `localhost:5432` | — | Database (db: `teable`, user: `teable`) |
|
||||
| Redis | `localhost:6379` | — | BullMQ queue for automation runs, distributed lock |
|
||||
|
||||
- The frontend reads `SOCKET_PORT=3000` from `.env` to point its websocket at the backend. Don't let it fall back to `3001`.
|
||||
- End users browse `:3002`. The backend `:3000` is the API only (`BACKEND_SKIP_NEXT_START=true` disables the embedded Next server).
|
||||
|
||||
## Health Checks
|
||||
|
||||
```bash
|
||||
# Backend API up?
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/auth/signin -X POST \
|
||||
-H 'Content-Type: application/json' -d '{}' # expect 401 (endpoint exists, bad creds)
|
||||
|
||||
# Frontend up?
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:3002 # expect 307 or 200
|
||||
|
||||
# Ports listening?
|
||||
ss -tlnp | grep -E ':3000|:3002|:5432|:6379'
|
||||
```
|
||||
|
||||
If `:3000` is unreachable but systemd shows `active`, the worker likely OOM-crashed while the `pnpm` parent stayed alive:
|
||||
```bash
|
||||
systemctl --user stop teable-backend.service
|
||||
pkill -9 -f 'dist/index.js'
|
||||
systemctl --user start teable-backend.service
|
||||
```
|
||||
|
||||
## Acquiring a PAT (Personal Access Token)
|
||||
|
||||
The CLI authenticates with a PAT (`teable_{id}_{encryptedSign}`). Two ways to get one:
|
||||
|
||||
### Option A — via the CLI (needs a session cookie or existing token)
|
||||
```bash
|
||||
inkreach-teable auth token create --name my-cli \
|
||||
--scopes automation|read,automation|create,automation|update,automation|delete,table|read,record|read,record|create,record|update \
|
||||
--base-ids bseXXX --expires 2099-12-31
|
||||
# → prints full token ONCE
|
||||
```
|
||||
Note: `auth token list/create` may require a **session** (browser login) rather than a PAT — if you get 403, use Option B.
|
||||
|
||||
### Option B — via the web UI (recommended for the first token)
|
||||
1. Log in to `http://localhost:3002` as an admin.
|
||||
2. User menu → **Settings** → **Personal Access Tokens**.
|
||||
3. Create a token with scopes: `automation|*`, `table|*`, `record|*`, `field|*`, `view|*` as needed. Limit to the base if desired.
|
||||
4. Copy the `teable_xxx_yyy` string.
|
||||
|
||||
### Configure the CLI
|
||||
```bash
|
||||
inkreach-teable auth login --endpoint http://localhost:3000 --token <PAT> --base-id bseXXX
|
||||
inkreach-teable auth status # verify
|
||||
```
|
||||
|
||||
## Reference Credentials (local dev)
|
||||
|
||||
> Never commit real secrets. The reference dev instance uses:
|
||||
> - Admin: `hr_admin@xingchen.tech` / (ask maintainer) — for browser login only.
|
||||
> - Base: `bseeZR7yiDNeMPCa4CN` (人事薪资管理) — the test base with ~600 automations.
|
||||
> - Test table: `tblzPOXQvUruQosKNP5` (has button fields for automation testing).
|
||||
|
||||
For CLI use, **always prefer a PAT** over the admin password.
|
||||
|
||||
## Permissions
|
||||
|
||||
- PAT scope ∩ base role = effective permission. To manage automations you need `automation|read/create/update/delete` scopes **and** a role of editor+ on the base.
|
||||
- Roles: owner (all), creator (all on the base), editor (read/create/update), commenter (read), viewer (read).
|
||||
- Workflows auto-create an internal token on activation (name `workflow:{id}`, scope limited to table/record read+write) — used by action nodes, not for CLI auth.
|
||||
|
||||
## Troubleshooting Access
|
||||
|
||||
| Symptom | Fix |
|
||||
|---------|-----|
|
||||
| `401 Unauthorized` | Token wrong/expired → recreate PAT, `auth login` again |
|
||||
| `403 Forbidden` | Token lacks scope, or base role too low → widen scopes / raise role |
|
||||
| `404 Base not found` | Wrong base id, or endpoint points to a different instance → `auth status`, check `--base-id` |
|
||||
| Connection refused | Backend down → health check, restart service (see [development.md](development.md)) |
|
||||
@@ -0,0 +1,327 @@
|
||||
# Tables, Fields, Records & Views
|
||||
|
||||
Complete guide for building table structures and working with data via the `inkreach-teable` CLI.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Minimal walkthrough — create a table with fields in one shot, then insert a record:
|
||||
|
||||
```bash
|
||||
# 1. Create a "Tasks" table with 4 fields
|
||||
inkreach-teable table create --name "Tasks" --base-id bseXXX \
|
||||
--fields '[
|
||||
{"name":"Title","type":"singleLineText"},
|
||||
{"name":"Status","type":"singleSelect","options":{"choices":["Todo","Doing","Done"]}},
|
||||
{"name":"Due Date","type":"date"},
|
||||
{"name":"Est Hours","type":"number","options":{"formatting":{"precision":1}}}
|
||||
]'
|
||||
|
||||
# 2. Insert a record
|
||||
inkreach-teable record create --table-id tblXXX \
|
||||
--fields '{"Title":"Ship sign-up flow","Status":"Doing","Due Date":"2025-06-15","Est Hours":3.5}'
|
||||
|
||||
# 3. Verify
|
||||
inkreach-teable record get --table-id tblXXX
|
||||
```
|
||||
|
||||
> **Tip**: `table create --fields` creates the table AND all fields atomically. For existing tables, use `field create`.
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
|
||||
### Create
|
||||
|
||||
```bash
|
||||
inkreach-teable table create --name "<name>" [--base-id <id>] \
|
||||
[--fields '<json>'] [--description <desc>]
|
||||
```
|
||||
|
||||
| Flag | Required | Description |
|
||||
|------|----------|-------------|
|
||||
| `--name` | yes | Display name (unique within base) |
|
||||
| `--base-id` | no | Defaults to stored config |
|
||||
| `--fields` | no | Array of field objects (see below) |
|
||||
| `--description` | no | Optional description |
|
||||
|
||||
**`--fields` format**: JSON array of `{name, type, [options], [description]}` objects. Allowed types and aliases:
|
||||
|
||||
| Shorthand | Full type | Shorthand | Full type |
|
||||
|-----------|-----------|-----------|-----------|
|
||||
| `text`, `str` | `singleLineText` | `long` | `longText` |
|
||||
| `num`, `int` | `number` | `check`, `bool` | `checkbox` |
|
||||
| `sel`, `select` | `singleSelect` | `multi`, `tags` | `multipleSelect` |
|
||||
| `date` | `date` | `rate`, `star` | `rating` |
|
||||
| `user`, `person` | `user` | `file`, `attach` | `attachment` |
|
||||
| `link`, `rel` | `link` | `rollup` | `rollup` |
|
||||
| `formula` | `formula` | `auto` | `autoNumber` |
|
||||
| `created` | `createdTime` | `modified` | `lastModifiedTime` |
|
||||
| `createdBy` | `createdBy` | `modifiedBy` | `lastModifiedBy` |
|
||||
| `button` | `button` | `condRollup` | `conditionalRollup` |
|
||||
|
||||
**Compact shorthand** (in `--fields` only): `"name:type"` string format:
|
||||
|
||||
```bash
|
||||
inkreach-teable table create --name "Projects" --fields '["Name:text","Budget:num","Active:check"]'
|
||||
```
|
||||
|
||||
With select options: `"Status:sel:Todo,Doing,Done"`, `"Tags:mul:Bug,Feature,UI"`
|
||||
|
||||
### Update & Delete
|
||||
|
||||
```bash
|
||||
inkreach-teable table update <tableId> --name "<newName>" [--description <d>]
|
||||
inkreach-teable table delete <tableId>
|
||||
```
|
||||
|
||||
| Operation | Idempotent? | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| `update` | yes | Only changes name/description |
|
||||
| `delete` | **DESTRUCTIVE** | Deletes all records + fields; irreversible |
|
||||
|
||||
### List & Get
|
||||
|
||||
```bash
|
||||
inkreach-teable table list [--base-id <id>]
|
||||
inkreach-teable table get <tableId>
|
||||
```
|
||||
|
||||
`table get` returns `dbTableName` — the internal SQL table name used in `sql` queries (e.g., `SELECT * FROM "bseXXX"."tbl_abc"`).
|
||||
|
||||
---
|
||||
|
||||
## Fields
|
||||
|
||||
### Create
|
||||
|
||||
```bash
|
||||
inkreach-teable field create --table-id <tid> --name "<name>" \
|
||||
--type <type> [--options '<json>'] [--description <d>]
|
||||
```
|
||||
|
||||
| Flag | Required | Notes |
|
||||
|------|----------|-------|
|
||||
| `--table-id` | yes | Target table |
|
||||
| `--name` | yes | Field display name |
|
||||
| `--type` | yes | Full type name or alias (see [field-types.md](../api-reference/field-types.md)) |
|
||||
| `--options` | depends on type | JSON object; see type-specific shapes below |
|
||||
| `--description` | no | Optional |
|
||||
|
||||
**Type inference from name**: when `--type` is omitted, the CLI infers from the field name:
|
||||
|
||||
| Name pattern | Inferred type | Name pattern | Inferred type |
|
||||
|-------------|---------------|-------------|---------------|
|
||||
| `status`, `priority`, `stage`, `category`, `type`, `level` | `singleSelect` | `tag`, `label` | `multipleSelect` |
|
||||
| `description`, `notes`, `comment`, `content`, `body` | `longText` | `count`, `amount`, `price`, `quantity`, `total`, `age` | `number` |
|
||||
| `rating`, `score`, `stars` | `rating` | `is*`, `has*`, `active`, `enabled`, `done`, `completed` | `checkbox` |
|
||||
| `assignee`, `owner`, `user` | `user` | `attachment`, `file`, `image`, `photo`, `document` | `attachment` |
|
||||
| `email` | `singleLineText` | `url`, `link` (without table context) | `singleLineText` |
|
||||
| `created time` / `created` | `createdTime` | `last modified` / `updated` | `lastModifiedTime` |
|
||||
| `created by`, `author` | `createdBy` | `modified by`, `updated by` | `lastModifiedBy` |
|
||||
|
||||
**Options by type** (most common):
|
||||
|
||||
| Type | Required options | Shape |
|
||||
|------|-----------------|-------|
|
||||
| `singleSelect` / `multipleSelect` | `choices` | `{"choices":["A","B","C"]}` or `{"choices":[{"name":"A","color":"blue"}]}` — colors auto-assigned if omitted |
|
||||
| `number` | none (defaults integer) | `{"formatting":{"precision":2}}` for decimal |
|
||||
| `rating` | none (defaults max=5, star, yellow) | `{"max":5,"icon":"star","color":"yellowBright"}` |
|
||||
| `date` | none | `{"formatting":{"date":"YYYY-MM-DD","time":"None","timeZone":"UTC"}}` |
|
||||
| `link` | `foreignTableId` / `foreignTableName` | `{"foreignTableId":"tblXXX","relationship":"manyOne"}` |
|
||||
| `formula` | `expression` | `{"expression":"{fieldId}+{otherFieldId}"}` |
|
||||
| `rollup` | via `--lookup-options` | See [field-types.md](../api-reference/field-types.md) |
|
||||
| `button` | `workflow` | `{"label":"Click","color":"blue","workflow":{"id":"wflXXX"}}` |
|
||||
|
||||
**Name resolution**: for `link` fields, use `foreignTableName` instead of `foreignTableId` — the CLI resolves it automatically:
|
||||
|
||||
```bash
|
||||
inkreach-teable field create --table-id tblXXX --name "Project" --type link \
|
||||
--options '{"foreignTableName":"Projects","relationship":"manyOne"}'
|
||||
```
|
||||
|
||||
### Update & Delete
|
||||
|
||||
```bash
|
||||
inkreach-teable field update <fieldId> [--name "<n>"] [--options '<json>']
|
||||
inkreach-teable field delete <fieldId>
|
||||
```
|
||||
|
||||
| Detail | Rule |
|
||||
|--------|------|
|
||||
| `--name` only | Convenience; for other props use `--options` |
|
||||
| `--options` merge | Partial updates; omitted keys are preserved |
|
||||
| `field delete` | Irreversible — all cell data in that field is lost |
|
||||
|
||||
---
|
||||
|
||||
## Records
|
||||
|
||||
### Get (read)
|
||||
|
||||
```bash
|
||||
# All records (paginated)
|
||||
inkreach-teable record get --table-id <tid> [--filter '<json>'] [--projection '<json>'] \
|
||||
[--page-size <n>] [--page <n>] [--search <kw>]
|
||||
|
||||
# Single record by ID
|
||||
inkreach-teable record get-one <recordId>
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--filter` | Filter expression: `{"fieldId":{"operator":"value"}}` |
|
||||
| `--projection` | JSON array of field IDs to return: `'["fldXXX","fldYYY"]'` |
|
||||
| `--page-size` | Records per page (default 100) |
|
||||
| `--search` | Keyword search: `"keyword"` (auto-wraps to `{"value":"keyword"}`) |
|
||||
| `--view-id` | Fetch in view order with view's filter/sort/group applied |
|
||||
|
||||
### Create
|
||||
|
||||
```bash
|
||||
# Single record
|
||||
inkreach-teable record create --table-id <tid> --fields '<json>'
|
||||
|
||||
# Batch (up to 2000)
|
||||
inkreach-teable record create --table-id <tid> --records '<json>'
|
||||
```
|
||||
|
||||
**`--fields` format** (single): `{"FieldName":"value","Another":"value"}` — field names resolve automatically.
|
||||
|
||||
**Value semantics**:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `"string"` | Normal value |
|
||||
| `null` | **Clear** the cell |
|
||||
| `""` (empty string) | **Skip** (no change; update only) |
|
||||
|
||||
> **Important**: `""` is skip, not clear. Use `null` to empty a cell. For checkbox: `true` = checked, `null` = unchecked (NOT `false`).
|
||||
|
||||
**`--typecast`**: auto-converts human-readable values to internal IDs. Essential for `user` and `link` fields:
|
||||
|
||||
```bash
|
||||
# Without typecast — must use IDs
|
||||
inkreach-teable record create --table-id tblXXX --fields '{"Assignee":{"id":"usrXXX","title":"Alice"}}'
|
||||
|
||||
# With typecast — use display names
|
||||
inkreach-teable record create --table-id tblXXX --fields '{"Assignee":"Alice"}' --typecast
|
||||
```
|
||||
|
||||
**`--records` format** (batch): `{"header":["Field1","Field2"],"records":[["val1","val2"],["val3","val4"]]}`
|
||||
|
||||
### Update
|
||||
|
||||
```bash
|
||||
inkreach-teable record update --table-id <tid> --records '<json>'
|
||||
```
|
||||
|
||||
**`--records` format**: `[{"id":"recXXX","fields":{"FieldName":"newValue"}}]` — array of `{id, fields}` objects. Batch up to 2000.
|
||||
|
||||
**Record reordering**: pass `--order` with `viewId`, `anchorId`, `position`:
|
||||
|
||||
```bash
|
||||
inkreach-teable record update --table-id tblXXX \
|
||||
--records '[{"id":"recXXX","fields":{}}]' \
|
||||
--order '{"viewId":"viwXXX","anchorId":"recYYY","position":"before"}'
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
```bash
|
||||
inkreach-teable record delete --table-id <tid> --record-ids "recA,recB,recC"
|
||||
```
|
||||
|
||||
Max 1000 IDs per call. Irreversible.
|
||||
|
||||
---
|
||||
|
||||
## Views
|
||||
|
||||
### List & Get
|
||||
|
||||
```bash
|
||||
inkreach-teable view list <tableId>
|
||||
inkreach-teable view get <viewId>
|
||||
```
|
||||
|
||||
### Create
|
||||
|
||||
```bash
|
||||
inkreach-teable view create --table-id <tid> --name "<name>" [--type <t>]
|
||||
```
|
||||
|
||||
| `--type` | Description |
|
||||
|----------|-------------|
|
||||
| `grid` | Default spreadsheet view |
|
||||
| `kanban` | Board (requires a singleSelect/link grouping field) |
|
||||
| `form` | Form input view |
|
||||
| `gallery` | Card gallery |
|
||||
| `calendar` | Calendar (requires a date field for grouping) |
|
||||
|
||||
### Update (configure filters, sorts, grouping)
|
||||
|
||||
```bash
|
||||
inkreach-teable view update <viewId> \
|
||||
[--name "<n>"] \
|
||||
[--filter '<json>'] \
|
||||
[--sort '<json>'] \
|
||||
[--group '<json>'] \
|
||||
[--column-meta '<json>']
|
||||
```
|
||||
|
||||
**Filters**: `{"conjunction":"and","filterSet":[{"fieldId":"fldXXX","operator":"is","value":"Done"}]}`
|
||||
|
||||
**Sorts**: `[{"fieldId":"fldXXX","order":"asc"}]`
|
||||
|
||||
**Group**: `[{"fieldId":"fldXXX","order":"asc"}]`
|
||||
|
||||
**Column meta**: controls visibility, width, order of fields in the view.
|
||||
|
||||
### Delete
|
||||
|
||||
```bash
|
||||
inkreach-teable view delete <viewId>
|
||||
```
|
||||
|
||||
Deleting a view does NOT delete the table or its data — only the view configuration.
|
||||
|
||||
---
|
||||
|
||||
## Value Formats Quick Reference
|
||||
|
||||
| Field Type | Value format | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| `singleLineText` / `longText` | `"string"` | |
|
||||
| `number` | `42` or `3.14` | Integer or float |
|
||||
| `checkbox` | `true` / `null` | NOT `false` for unchecked |
|
||||
| `date` | `"2025-06-15"` or `"2025-06-15T09:00:00.000Z"` | ISO 8601; `"now"` maps to current time |
|
||||
| `singleSelect` | `"Option Name"` | (string) |
|
||||
| `multipleSelect` | `["Tag A","Tag B"]` | (array of strings) |
|
||||
| `rating` | `3` | Integer 1..max |
|
||||
| `attachment` | `[{"name":"f.png","token":"xxx"}]` | Always array of objects; update **replaces all** |
|
||||
| `user` | `{"id":"usrXXX","title":"Alice"}` | With `--typecast`: just `"Alice"` or `"alice@example.com"` |
|
||||
| `link` | `[{"id":"recXXX"}]` | With `--typecast`: just `"Linked Record Name"` |
|
||||
| `formula` / `rollup` / `lookup` | Read-only | Cannot be set via record create/update |
|
||||
| `autoNumber` / `createdBy` / `createdTime` / `lastModifiedTime` / `lastModifiedBy` | Auto-populated | Cannot be set manually |
|
||||
| `button` | `{"count":0}` | Read-only; incremented by button clicks |
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| User intent | Correct approach | Wrong approach |
|
||||
|-------------|-----------------|----------------|
|
||||
| Create a table with fields | `table create --fields '[...]'` — one call | Multiple `field create` calls (slower, more error-prone) |
|
||||
| Use display names for linked records | `record create --fields '{"Project":"Acme"}' --typecast` | Manually looking up `recXXX` IDs |
|
||||
| Use email to assign user | `record create --typecast --fields '{"Assignee":"alice@co.com"}'` | Looking up `usrXXX` IDs |
|
||||
| Clear a cell | `null` in the fields object | `""` (which means skip!) |
|
||||
| Update one field, leave others | `--records '[{"id":"recXXX","fields":{"Status":"Done"}}]'` | Re-sending all fields (unnecessary, slower) |
|
||||
| Fetch only 2 columns | `record get --projection '["fldXXX","fldYYY"]'` | Fetching all fields (wastes bandwidth) |
|
||||
| Find records by keyword | `record get --search "keyword"` | `record get` then filtering manually |
|
||||
| Add a select field with colored options | `--options '{"choices":["A","B"]}'` — colors auto | Manually specifying color for every choice |
|
||||
| Make a link field to another table | `--type link --options '{"foreignTableName":"Target","relationship":"manyOne"}'` | Using IDs + manual lookup |
|
||||
| Reorder records in a view | `record update --order '{"viewId":...,"anchorId":...,"position":"before"}'` | Deleting and re-creating records |
|
||||
| View data in kanban board | `view create --type kanban` then `view update --group` | Creating special columns |
|
||||
| Set datetime in a date field | `"2025-06-15T14:30:00.000Z"` | `"2025-06-15 14:30"` (non-ISO; may be rejected) |
|
||||
| Update attachments | Always pass the full array (replaces all) | Trying to add/remove individual attachments |
|
||||
| Batch insert 500 records | One `record create --records` call | 500 separate `record create` calls |
|
||||
Reference in New Issue
Block a user