Files
yeuimu 76ef02d659 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.
2026-09-23 04:07:39 +00:00

210 lines
9.4 KiB
Markdown

# 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() };
'
```