Wrapping --config in {"config":{...}} deploys a silent empty script
(succeeds in ~1ms, result {}). Document the trap + verification recipe
(workflow get --json, nodes[].config.code non-empty).
Engine gap: the 'automation.record.update' event carries changedFieldIds
(and now oldFields), but RecordUpdatedTrigger.accumulate() dropped both —
scripts had no way to see what changed or pre-update values, despite the
documented trigger output (trigger-types.md).
- record-updated/record-condition/record-created-or-updated triggers:
pass changedFieldIds + oldFields through to accumulate()
- batch-buffer TriggerEvent += oldFields; batch flush records[] entries
now carry changedFieldIds + oldFields per record
- v2 updateRecord/updateRecords: emit oldFields (pre-update snapshot)
- v1 controller updateRecord/updateRecords: snapshot before-fields via
getRecordsById, compute changedFieldIds (deep-equal diff, same
semantics as v2) instead of hardcoded [] — v1/v2 parity
- new spec record-updated.trigger.spec.ts (4 tests, passthrough +
watch-filter + table isolation); workflow suite 16 files / 99 tests
- trigger-types.md: document semantics + honest scope (REST API writes;
ShareDB grid edits do not fire this trigger)
Live-verified: PATCH 流转角色 → trigger_input.changedFieldIds=[fld...],
oldFields=[售前客服] → record=[技术]; script reads oldRole via
triggerInput; fieldIds watch-filter blocks non-watched fields.
commander stores --code-file as opts.codeFile (kebab keys are never set),
so 'action update --code-file' silently no-opped (workflow.ts read
opts['code-file']) and 'auth token create --base-ids/--full-access'
never applied (auth.ts read the kebab keys). The 'action add' path was
unaffected because buildActionConfig's pick() checks both forms.
- workflow.ts action update: opts['code-file'] ?? opts.codeFile
- auth.ts token create: baseIds/fullAccess dual-form read
- verified live: action update --code-file now persists the new script
(was silently keeping old code); commander dual-key behavior confirmed
by direct parse (kebab keys absent, camelCase present, and this
commander version drops the --base-ids VALUE entirely — the dual read
covers both)
- cli-reference.md: field get/update/delete now <tableId> <fieldId>;
new field visible-choices command; note --options goes via convert API
- tables-fields.md: correct update/delete signatures; options-rewrite
semantics (convert API, whole-replace not merge); new section on
select-choice visibilityFilter dependency (read/write/evaluate,
backend 400 guards, write-time enforcement message)
- FieldOpenApiV2Service.convertField now calls the shared V1 validator
(assertSelectVisibilityConfigValid) before executing the update command.
Previously only the V1 path validated self-reference / dependency cycles,
so invalid configs could be persisted through V2 (verified live: a
self-referencing choice was accepted with 200).
- V1 assertSelectVisibilityConfigValid private -> public (shared guard).
- Unit tests: 3 new cases in field-open-api-v2.service.spec.ts (guard
invoked with exact args; rejects before executing when validator throws;
called with raw type/options for non-select conversions). 63/63 pass;
tsc build config 0 errors.
- inkreach-cli: field get/update/delete targeted non-existent /api/field/:id
routes; now use /api/table/:tableId/field/:fieldId. update --options goes
through the convert API (the PATCH update API strips options by design).
New: convertField + getVisibleChoices client methods, field visible-choices
command (evaluates choice visibility for a record).
getEffectiveConfig was rewritten in 948a20b88 to fetch all configs via
findMany (Prisma cannot reliably WHERE on nullable String? with ''), but
the spec still mocked the old findFirst call chain — the table-override
test silently fell back to defaults. Mocks now return the config array.
Bundle plugins run inside the base page (not a sandboxed iframe), so their
declared apiOrigins must be in the PAGE's connect-src. Previously the page
CSP was hardcoded static — plugins connecting to new APIs required a code
change + rebuild.
- GET /api/plugin/csp-origins: union of all plugins' manifest apiOrigins
- [[...slug]].tsx getServerSideProps: fetches origins via ssrApi, merges
with static fallbacks ('self' + 8001 hosts), builds CSP dynamically
- PUT /api/plugin/:pluginId accepts apiOrigins (merged into manifest JSON)
- CLI: plugin csp list / plugin csp set <id> --origins '[...]'
- @DisabledPermission on plugin GET/PUT routes (default-deny guard blocked
PAT auth); ownership still enforced via manageablePluginCreatedBy()
- CLI: plugin dev list/allow/revoke (userId or email)
- Backend: PluginDeveloperService.addDeveloper is now idempotent
(returns existing record instead of unique-constraint 500)
- Backend: removeDeveloper silently no-ops on P2025 (not found)
- Controller: route api/plugin-developers (avoids :pluginId collision),
@DisabledPermission so admin check is the sole gate
Scripts can now branch on WHO triggered the workflow and their role in the
base, without an extra API call (the workflow token has no role-read scope).
New triggerInput fields (user-initiated triggers only: buttonClick /
recordCreated / recordUpdated / recordCondition / formSubmitted):
- _triggerUserRoles: string[] (enabled role display names in this base)
- _triggerUserIsAdmin: boolean (space owner / AM admin / AM-disabled)
Absent for scheduledTime / webhook (no user context).
Implementation:
- trigger-user-info.ts: resolveTriggerUserInfo(prisma, baseId, userId) —
standalone, unit-tested (8 tests). Mirrors the admin logic in
AuthorityMatrixPermissionService but returns only role names + bool.
- workflow-runner.service.ts: resolve once at first action node, merge into
enrichedInput (both execute() and test() paths).
- skill action-types.md: document the fields + script branching example.
- action add/update: new --dependencies <json> option
- parseDependencies accepts JSON, name@version shorthand, or bare names
- update is incremental; --dependencies replaces the whole array
- docs: deployment/systemd.md + skill SKILL/action-types/development
document the npm-deps feature and the service-PATH requirement
(spawn npm ENOENT without nvm bin dir on PATH)
The records API requires search[]=value (tuple), not search=value
(string). Without this, the CLI returned 400 'expected tuple, received
string'. Same fix already exists in the workflow getRecords action.
The API controller reads recordIds from @Query(), not @Body.
CLI was sending { records: ids } as JSON body, causing:
'expected array, received undefined at "recordIds"'
Fixed: rename deleteRecord→deleteRecords, send recordIds as
repeated query params (?recordIds=rec1&recordIds=rec2).
The skill description was 1033 chars, exceeding pi's 1024-char limit and
causing a 'Skill conflicts: description exceeds 1024 characters' warning,
which prevents the skill from loading/triggering reliably.
Shortened to 990 chars while preserving all trigger-relevant keywords
(inkreach-teable/Teable, ID patterns bseXXX/tblXXX/..., configure
tables/automations, dev/deploy, systemd). Also fixed YAML folded-scalar
artifacts that injected stray spaces after slashes
('/ formSubmitted', '/ httpRequest') by avoiding line breaks after '/'.
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).
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.