skill(iter-1): fix CLI getRecords flag + docs from eval feedback
Ran 4 end-to-end evals (skill-creator flow): all 23 assertions passed (100%). The evals exposed 3 issues, now fixed: 1. CLI bug: getRecords/createRecord/updateRecord --target-table flag was read as opts['target-table'] but commander stores it camelCase (opts.targetTable). Added a pick() helper accepting both forms. 2. trigger-types.md: buttonClick output said record.fields.* implying all fields; actually record.fields only contains the button field's count. Documented the back-channel API fetch pattern for other fields. 3. automation-guide.md: documented that 'workflow test' uses mock input with empty fields, so upstream outputs are absent in test mode — use real fire for data-dependent validation. Also: corrected button field options (workflow.id object, not workflowId).
This commit is contained in:
@@ -125,7 +125,7 @@ These differ from official teable and from the (stale) `docs/automation/triggers
|
|||||||
1. **Use `inkreach-teable` CLI, never `teable` CLI for automations** — official CLI uses incompatible contract (filter/watchFieldIds/recordMatchesConditions) and will 404 or corrupt config.
|
1. **Use `inkreach-teable` CLI, never `teable` CLI for automations** — official CLI uses incompatible contract (filter/watchFieldIds/recordMatchesConditions) and will 404 or corrupt config.
|
||||||
2. **Read [trigger-types.md](api-reference/trigger-types.md) before configuring a trigger** — contains exact config keys per trigger; guessing leads to silent misfires.
|
2. **Read [trigger-types.md](api-reference/trigger-types.md) before configuring a trigger** — contains exact config keys per trigger; guessing leads to silent misfires.
|
||||||
3. **Build the full chain then activate** — `workflow create` → `trigger set` → `action add` → `activate`. Editing nodes after activation requires re-`activate` to snapshot.
|
3. **Build the full chain then activate** — `workflow create` → `trigger set` → `action add` → `activate`. Editing nodes after activation requires re-`activate` to snapshot.
|
||||||
4. **For `buttonClick`, ensure the field is a Button field bound to this workflow** — create via `field create --type button` with `options.workflowId`, or it won't render/click. Verify `workflow.isActive` (the field options capture a stale `isActive` snapshot at creation).
|
4. **For `buttonClick`, ensure the field is a Button field bound to this workflow** — create via `field create --type button` with `options.workflow.id` (the `workflow` value is an **object** `{id, name?}`, NOT a bare `workflowId` string — matching the DB shape), or the field won't render/click. Note: field `options` captures a stale `isActive` snapshot at creation; the API validates the DB `isActive` at click time instead, so a deactivated workflow returns 400.
|
||||||
5. **Read [action-types.md](api-reference/action-types.md) before writing scripts** — covers sandbox globals, variable references `{{nodeIndex.path}}`, and the `_accessToken`/`_apiBaseUrl` back-channel.
|
5. **Read [action-types.md](api-reference/action-types.md) before writing scripts** — covers sandbox globals, variable references `{{nodeIndex.path}}`, and the `_accessToken`/`_apiBaseUrl` back-channel.
|
||||||
|
|
||||||
## 5. Common Errors & Recovery
|
## 5. Common Errors & Recovery
|
||||||
|
|||||||
@@ -176,12 +176,21 @@ inkreach-teable trigger set <wfId> --type buttonClick --table-id tblXXX --field-
|
|||||||
```json
|
```json
|
||||||
{ "tableId": "tblXXX", "fieldIds": ["fldGo"] }
|
{ "tableId": "tblXXX", "fieldIds": ["fldGo"] }
|
||||||
```
|
```
|
||||||
- The field **must be a Button-type field** whose `options.workflowId` points to this workflow.
|
- 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 '{"workflowId":"wflXXX","label":"Go"}'`
|
- 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`).
|
**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`.
|
**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.
|
⚠️ 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -147,6 +147,19 @@ inkreach-teable runs get wflXXX <executionId>
|
|||||||
inkreach-teable runs rerun wflXXX <executionId>
|
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)
|
## Variable References (cross-node data)
|
||||||
|
|
||||||
An action can reference upstream nodes' output in string fields via `{{nodeIndex.path}}`:
|
An action can reference upstream nodes' output in string fields via `{{nodeIndex.path}}`:
|
||||||
|
|||||||
Vendored
+18
-8
@@ -466,11 +466,15 @@ function readFile(p) {
|
|||||||
/** Build action config from granular flags. */
|
/** Build action config from granular flags. */
|
||||||
function buildActionConfig(opts) {
|
function buildActionConfig(opts) {
|
||||||
const type = opts.type;
|
const type = opts.type;
|
||||||
|
// Helper: commander stores --kebab-case flags as camelCase in opts (e.g.
|
||||||
|
// --target-table -> opts.targetTable). Accept both forms for safety.
|
||||||
|
const pick = (k) => (opts[k] ?? opts[toCamel(k)]);
|
||||||
const config = {};
|
const config = {};
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'script': {
|
case 'script': {
|
||||||
if (opts['code-file'])
|
const codeFile = pick('code-file');
|
||||||
config.code = readFile(opts['code-file']);
|
if (codeFile)
|
||||||
|
config.code = readFile(codeFile);
|
||||||
else if (opts.code)
|
else if (opts.code)
|
||||||
config.code = opts.code;
|
config.code = opts.code;
|
||||||
if (opts.timeout)
|
if (opts.timeout)
|
||||||
@@ -493,21 +497,24 @@ function buildActionConfig(opts) {
|
|||||||
config.method = opts.method;
|
config.method = opts.method;
|
||||||
if (opts.headers)
|
if (opts.headers)
|
||||||
config.headers = JSON.parse(opts.headers);
|
config.headers = JSON.parse(opts.headers);
|
||||||
if (opts['req-body'])
|
const reqBody = pick('req-body');
|
||||||
config.body = JSON.parse(opts['req-body']);
|
if (reqBody)
|
||||||
|
config.body = JSON.parse(reqBody);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'createRecord':
|
case 'createRecord':
|
||||||
case 'updateRecord': {
|
case 'updateRecord': {
|
||||||
if (opts['target-table'])
|
const tt = pick('target-table');
|
||||||
config.tableId = opts['target-table'];
|
if (tt)
|
||||||
|
config.tableId = tt;
|
||||||
if (opts.records)
|
if (opts.records)
|
||||||
config.records = JSON.parse(opts.records);
|
config.records = JSON.parse(opts.records);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'getRecords': {
|
case 'getRecords': {
|
||||||
if (opts['target-table'])
|
const tt = pick('target-table');
|
||||||
config.tableId = opts['target-table'];
|
if (tt)
|
||||||
|
config.tableId = tt;
|
||||||
if (opts.query)
|
if (opts.query)
|
||||||
config.query = JSON.parse(opts.query);
|
config.query = JSON.parse(opts.query);
|
||||||
break;
|
break;
|
||||||
@@ -518,3 +525,6 @@ function buildActionConfig(opts) {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
exports.buildActionConfig = buildActionConfig;
|
exports.buildActionConfig = buildActionConfig;
|
||||||
|
function toCamel(s) {
|
||||||
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|||||||
@@ -501,10 +501,14 @@ function readFile(p: string): string {
|
|||||||
/** Build action config from granular flags. */
|
/** Build action config from granular flags. */
|
||||||
export function buildActionConfig(opts: Record<string, string>): Record<string, unknown> {
|
export function buildActionConfig(opts: Record<string, string>): Record<string, unknown> {
|
||||||
const type = opts.type;
|
const type = opts.type;
|
||||||
|
// Helper: commander stores --kebab-case flags as camelCase in opts (e.g.
|
||||||
|
// --target-table -> opts.targetTable). Accept both forms for safety.
|
||||||
|
const pick = (k: string): string | undefined => (opts[k] ?? opts[toCamel(k)]) as string | undefined;
|
||||||
const config: Record<string, unknown> = {};
|
const config: Record<string, unknown> = {};
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'script': {
|
case 'script': {
|
||||||
if (opts['code-file']) config.code = readFile(opts['code-file']);
|
const codeFile = pick('code-file');
|
||||||
|
if (codeFile) config.code = readFile(codeFile);
|
||||||
else if (opts.code) config.code = opts.code;
|
else if (opts.code) config.code = opts.code;
|
||||||
if (opts.timeout) config.timeout = Number(opts.timeout);
|
if (opts.timeout) config.timeout = Number(opts.timeout);
|
||||||
break;
|
break;
|
||||||
@@ -519,17 +523,20 @@ export function buildActionConfig(opts: Record<string, string>): Record<string,
|
|||||||
if (opts.url) config.url = opts.url;
|
if (opts.url) config.url = opts.url;
|
||||||
if (opts.method) config.method = opts.method;
|
if (opts.method) config.method = opts.method;
|
||||||
if (opts.headers) config.headers = JSON.parse(opts.headers);
|
if (opts.headers) config.headers = JSON.parse(opts.headers);
|
||||||
if (opts['req-body']) config.body = JSON.parse(opts['req-body']);
|
const reqBody = pick('req-body');
|
||||||
|
if (reqBody) config.body = JSON.parse(reqBody);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'createRecord':
|
case 'createRecord':
|
||||||
case 'updateRecord': {
|
case 'updateRecord': {
|
||||||
if (opts['target-table']) config.tableId = opts['target-table'];
|
const tt = pick('target-table');
|
||||||
|
if (tt) config.tableId = tt;
|
||||||
if (opts.records) config.records = JSON.parse(opts.records);
|
if (opts.records) config.records = JSON.parse(opts.records);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'getRecords': {
|
case 'getRecords': {
|
||||||
if (opts['target-table']) config.tableId = opts['target-table'];
|
const tt = pick('target-table');
|
||||||
|
if (tt) config.tableId = tt;
|
||||||
if (opts.query) config.query = JSON.parse(opts.query);
|
if (opts.query) config.query = JSON.parse(opts.query);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -538,3 +545,7 @@ export function buildActionConfig(opts: Record<string, string>): Record<string,
|
|||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toCamel(s: string): string {
|
||||||
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user