Repository navigation
Record-change trigger plugin loads but flows never fire on data writes (7.4.1) #1491
Description
Summary
With the triggers capability enabled, @objectstack/plugin-trigger-record-change loads (appears in the started-plugins list) but record-change flows are never launched when records are created/updated through the REST data API. Object-level L2 lifecycle hooks fire correctly on the same writes, so the ObjectQL hook pipeline itself works — the gap is specifically in the trigger-plugin → automation wiring.
Scheduled flows are likely affected by the same wiring (couldn't be confirmed — cron-bound).
Environment
@objectstack/runtime7.4.1@objectstack/cli7.4.1@objectstack/service-automation7.4.1@objectstack/plugin-trigger-record-change7.4.1@objectstack/plugin-trigger-schedule7.4.1@objectstack/objectql7.4.1- Driver:
sqlite-wasm· Node v25.9.0 · launched viaobjectstack start - Repro app: https://github.com/objectstack-ai/hotcrm (flow
lead_assignment)
Repro
- App
requires: [..., 'triggers', ...]→RecordChangeTriggerPlugin+ScheduleTriggerPluginappear in the "Plugins: N loaded" banner. - Define a
record_changeflow whosestartnode config matches the documented contract resolved byresolveTriggerBinding():Body: a{ id: 'start', type: 'start', config: { objectName: 'crm_lead', triggerType: 'record-after-create' } }
decision+update_recordthat stamps a field (e.g.next_followup_date). POST /api/v1/data/crm_leadto create a record.- Poll the record for up to 30s.
Expected
The flow binds to the ObjectQL afterInsert hook (triggerTypeToHookEvent('record-after-create') === 'afterInsert') and runs, stamping the field.
Actual
The field is never written; updated_at is unchanged after 30s. The flow does not run. Same result for record-after-update flows (case escalation, opportunity approval, CSAT followup).
Evidence that the rest of the stack works
- L2 object hooks DO fire on REST writes: a
beforeInserthook oncrm_leadrecomputedrating(sentrating: "5", storedrating: "1"), proving ObjectQL lifecycle hooks run on the REST data path. - Build/registration is clean: 10 flows register; Studio reports zero metadata-validation issues.
triggerTypevalues map correctly:triggerTypeToHookEvent('record-after-create') → 'afterInsert','record-after-update' → 'afterUpdate'.- The data engine exposes
registerHook(confirmed at runtime:objectql engine = OK registerHook? function).
What I could trace (read-only, from dist)
service-automationresolveTriggerBinding()requiresstart.config.triggerTypestarting withrecord-(orflow.type === 'schedule'). ✅ satisfied.registerFlow()→activateFlowTrigger(); andregisterTrigger()re-binds already-registered flows, so plugin/flow registration ordering should be handled.RecordChangeTriggerPlugin.start()registers the trigger on thekernel:readyhook viaautomation.registerTrigger(...), thenstart(binding, cb)callsengine.registerHook(hookEvent, handler, { object, packageId }).
So the binding should occur, but empirically the handler never runs.
Blocker for diagnosis
packages/cli serve forces the kernel logger to level: 'silent' (const loggerConfig = { level: 'silent' }). This suppresses every logger.info/logger.warn from the automation engine and trigger plugin — including:
Trigger registered: record_change[record-change] bound flow '<name>' → afterInsertRecordChangeTriggerPlugin: ... record-change trigger NOT installed
so it's impossible to tell from objectstack start output whether the trigger registered, whether any flow bound, or whether it failed. Please consider honoring an env var (e.g. OS_LOG_LEVEL) or a --log-level flag to override the silent default, independent of this bug.
Hypotheses to check
kernel:readyhook inRecordChangeTriggerPlugin.start()not firing, orctx.getService('automation')returning a different instance than the one that registered the flows.engine.registerHook('afterInsert', …, { object })attaching to a hook channel that the REST/ObjectQL write path does not emit (event-name or per-object scoping mismatch between the trigger plugin and objectql 7.4.1).- Flow runs but
update_recordis rejected under the system/anonymous context used by the trigger (RLS), failing silently.
Suggested next step
Add an integration test in packages/plugins/plugin-trigger-record-change that boots a LiteKernel with automation + record-change trigger + a trivial flow, inserts a row, and asserts the flow executed — with logger at info so the bind/exec path is visible.
Activity
xuyushun441-sys commentedon Jun 1, 2026
Local diagnosis follow-up
Tried to get ground truth on whether the record-change trigger binds/fires.
1. Plugin loggers are independent of the kernel logger config
Patching
packages/cliserveto construct the Runtime with{ kernel: { logger: { level: 'info' } } }(instead of the hardcoded'silent') surfaced kernel/objectql logs (e.g. sandboxed-hook errors) but still showed none of the automation/trigger plugin's own logs:Trigger registered: record_change[record-change] bound flow …RecordChangeTriggerPlugin: … NOT installedSo the plugin
ctx.logger.{info,warn}stream is not governed by the kernel logger level — raising the kernel level is insufficient to observe trigger binding/registration. Whatever log-level override you add (per the request above) needs to reach the per-plugin context loggers too, otherwise this layer stays invisible inobjectstack start.2. Empirical result still: record-change flow never executes
With everything contract-correct (
triggerscapability loaded,triggerType: 'record-after-create', condition omitted), inserting acrm_leadvia REST does not run the bound flow (next_followup_datenever stamped,updated_atunchanged after 30s). Meanwhile object L2 hooks on the same write path do run (leadbeforeInsertrecomputedrating; an opportunitybeforeInserthook even threw and was logged). So ObjectQL lifecycle hooks fire — the record-change trigger→automation path does not.3. Unrelated observations surfaced while logs were visible (separate from this issue)
ReferenceErroron every write (body-only QuickJS scope). Fixed app-side by moving constants into the handler. Flagging because the failure mode is easy to hit and the silent default hides it — a clearer sandbox-scope error/hint at registration time would help.sqlite-wasmdriver, some inserts intermittently fail withCOMMIT; - cannot commit - no transaction is active(account creates hit this repeatedly while lead creates succeeded). Possibly nested-transaction handling under hooks/sharing. Can file separately with a focused repro if useful.Happy to add a minimal LiteKernel repro (automation + record-change trigger + trivial flow + insert assertion) if that would speed up triage.
feat(expression): ADR-0032 phase 1 — validate-by-default expression l…
docs(formula): document 7.6 expression changes (ADR-0032) (#1527)
xuyushun441-sys commentedon Jun 3, 2026
Still reproduces on
@objectstack 7.7.0(latest). Re-tested against theobjectstack-ai/templatesallaggregate:Startup logs show the automation service loads and registers all flows (
[Automation] flow pull: registry returned 30 flow(s)), but zero per-write trigger activity. Two independentrecord_changeflows do not fire on data writes:helpdesk_ai_triage_on_create— create a ticket withstatus: 'new'→ staysnew,ai_triage_atnull (should advance totriaged).helpdesk_escalate_angry_customer— setai_sentiment: 'angry'+priority: 'urgent'on a ticket → stays put (should flip toescalated). This one is a clean repro: pure field-equality criteria, a singleupdate_recordnode, no date comparison, noinvoke_function— so it isolates the trigger-dispatch path itself.Object lifecycle hooks and validations fire correctly on the same writes, so it's specifically the record-change → flow-engine dispatch that's not wired. Unchanged from 7.4.1 / 7.6.0.
test(trigger-record-change): end-to-end regression coverage for recor…
test(trigger-record-change): end-to-end regression coverage for recor…
test(trigger-record-change): end-to-end regression coverage for recor…
test(trigger-record-change): end-to-end regression coverage for recor…
fix(automation): record-change flows see multi-lookup fields + array-…
docs(skill): teach this season's flow anti-patterns in the automation…
docs(skill): verified anti-pattern deltas for formula + data authorin…
fix(automation): evaluateCondition decides the dialect from the sourc…
fix(spec): let a schemaless node own an expression-ledger entry (obje…
test(spec): fix the three unrunnable shapes the flow fixtures taught (o…
fix(formula): give an over-budget expression a size prescription, not…