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.
210 lines
9.4 KiB
Markdown
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() };
|
|
'
|
|
```
|