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/runtime 7.4.1
@objectstack/cli 7.4.1
@objectstack/service-automation 7.4.1
@objectstack/plugin-trigger-record-change 7.4.1
@objectstack/plugin-trigger-schedule 7.4.1
@objectstack/objectql 7.4.1
- Driver:
sqlite-wasm · Node v25.9.0 · launched via objectstack start
- Repro app: https://github.com/objectstack-ai/hotcrm (flow
lead_assignment)
Repro
- App
requires: [..., 'triggers', ...] → RecordChangeTriggerPlugin + ScheduleTriggerPlugin appear in the "Plugins: N loaded" banner.
- Define a
record_change flow whose start node config matches the documented contract resolved by resolveTriggerBinding():
{ id: 'start', type: 'start',
config: { objectName: 'crm_lead', triggerType: 'record-after-create' } }
Body: a decision + update_record that stamps a field (e.g. next_followup_date).
POST /api/v1/data/crm_lead to 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
beforeInsert hook on crm_lead recomputed rating (sent rating: "5", stored rating: "1"), proving ObjectQL lifecycle hooks run on the REST data path.
- Build/registration is clean: 10 flows register; Studio reports zero metadata-validation issues.
triggerType values 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-automation resolveTriggerBinding() requires start.config.triggerType starting with record- (or flow.type === 'schedule'). ✅ satisfied.
registerFlow() → activateFlowTrigger(); and registerTrigger() re-binds already-registered flows, so plugin/flow registration ordering should be handled.
RecordChangeTriggerPlugin.start() registers the trigger on the kernel:ready hook via automation.registerTrigger(...), then start(binding, cb) calls engine.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>' → afterInsert
RecordChangeTriggerPlugin: ... 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:ready hook in RecordChangeTriggerPlugin.start() not firing, or ctx.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_record is 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.
Summary
With the
triggerscapability enabled,@objectstack/plugin-trigger-record-changeloads (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.1sqlite-wasm· Node v25.9.0 · launched viaobjectstack startlead_assignment)Repro
requires: [..., 'triggers', ...]→RecordChangeTriggerPlugin+ScheduleTriggerPluginappear in the "Plugins: N loaded" banner.record_changeflow whosestartnode config matches the documented contract resolved byresolveTriggerBinding():decision+update_recordthat stamps a field (e.g.next_followup_date).POST /api/v1/data/crm_leadto create a record.Expected
The flow binds to the ObjectQL
afterInserthook (triggerTypeToHookEvent('record-after-create') === 'afterInsert') and runs, stamping the field.Actual
The field is never written;
updated_atis unchanged after 30s. The flow does not run. Same result forrecord-after-updateflows (case escalation, opportunity approval, CSAT followup).Evidence that the rest of the stack works
beforeInserthook oncrm_leadrecomputedrating(sentrating: "5", storedrating: "1"), proving ObjectQL lifecycle hooks run on the REST data path.triggerTypevalues map correctly:triggerTypeToHookEvent('record-after-create') → 'afterInsert','record-after-update' → 'afterUpdate'.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/cliserveforces the kernel logger tolevel: 'silent'(const loggerConfig = { level: 'silent' }). This suppresses everylogger.info/logger.warnfrom the automation engine and trigger plugin — including:Trigger registered: record_change[record-change] bound flow '<name>' → afterInsertRecordChangeTriggerPlugin: ... record-change trigger NOT installedso it's impossible to tell from
objectstack startoutput 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-levelflag 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).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-changethat boots a LiteKernel with automation + record-change trigger + a trivial flow, inserts a row, and asserts the flow executed — with logger atinfoso the bind/exec path is visible.