fix(workflow): suppress self-echo trigger duplication on workflow-written records

One user edit cascaded into 3-5 workflow executions and hourly sync jobs
generated 1000+ wasted runs/week (7d: 15.7k of 18k executions of the
ticket-status workflow ended in skipped no-op guards). Root cause: workflow
scripts write records via REST with a runner-signed token, and record-event
triggers could not distinguish those writes from human edits — every script
PATCH re-triggered its own workflow.

Attribution chain (new):
- WorkflowTokenService signs {workflowId, executionId} into the runner token
- jwt.strategy lifts it into CLS workflowContext (User-type branch)
- all 13 automation.record.* emit sites tag payloads with writeSource
- the 4 record triggers drop SELF echoes (writeSource.workflowId === own id)
  unconditionally; cross-workflow writes keep firing by default (chaining is
  load-bearing: order-sync建档 -> status workflow stamps 待处理, ~142 real
  actions/week). Opt-in stronger dedup via trigger config
  skipWorkflowWrites: true.

Verified live: human edit (处理反馈=正在核实) -> exactly 1 execution, script
flipped 流转状态 待处理->处理中, zero follow-up echoes; restore writes likewise
1 execution each with skipped no-ops. Unit: 14 new tests, 318 adjacent green.
This commit is contained in:
yeuimu
2026-09-18 02:28:11 +00:00
parent 1df830adc4
commit dc92b4cc8f
@@ -71,6 +71,13 @@ inkreach-teable trigger set <wfId> --type recordUpdated --table-id tblXXX \
- ⚠️ **Write-path coverage** (verified 2026-08): browser grid cell edits DO fire this trigger — every grid editor commit funnels through `record.updateCell` → the REST `updateRecord` API (no ShareDB op submit exists in the frontend). Covered surfaces: single/batch record update (REST v1+v2), grid paste (small `paste-by-id`, large `paste-*-stream`, range `paste`), grid cell clear (`clear` / `clear-by-id`), record duplicate, and programmatic/automation writes (workflow actions call the REST API). Form submissions intentionally emit ONLY the `formSubmitted` event (independent channel). Record deletions emit no automation event (no delete trigger category exists).
- Values resolve to `undefined` (not `[]`) if the emitting path could not snapshot before-values — scripts should guard `changedFieldIds ?? []`.
**Workflow-write echo suppression** (all 4 record triggers: `recordCreated` / `recordUpdated` / `recordCondition` / `recordCreatedOrUpdated`):
- Writes made BY workflow executions (script `fetch` with `triggerInput._accessToken`, `createRecords`/`updateRecords` actions) are tagged `writeSource: {workflowId, executionId}` on the event.
- **Self-echoes are always dropped**: a workflow never fires on its own writes (the script's PATCH re-triggering its own workflow was the duplication bug — one edit cascaded into 3-5 executions, every echo ending in a `skipped` no-op guard).
- Writes by OTHER workflows still fire by default — cross-workflow chaining is load-bearing (e.g. the order-sync job 建档 and this workflow stamping the initial 待处理). Opted-in events carry `triggerInput.writeSource.workflowId` for loop-guarding inside scripts.
- Config `"skipWorkflowWrites": true` additionally drops cross-workflow writes (stronger dedup when a workflow should react ONLY to human edits).
- Human grid edits, form/API/CLI writes are never tagged and always fire; suppressed writes still reach other listeners (e.g. record subscriptions) — only record-trigger handlers drop them.
---
## 3. recordCondition — When a record matches conditions