Skip to content

Record-change trigger plugin loads but flows never fire on data writes (7.4.1) #1491

Closed

Description

@xuyushun441-sys

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

  1. App requires: [..., 'triggers', ...] → RecordChangeTriggerPlugin + ScheduleTriggerPlugin appear in the "Plugins: N loaded" banner.
  2. 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).
  3. POST /api/v1/data/crm_lead to create a record.
  4. 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

  1. kernel:ready hook in RecordChangeTriggerPlugin.start() not firing, or ctx.getService('automation') returning a different instance than the one that registered the flows.
  2. 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).
  3. 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.

Activity

  1. xuyushun441-sys commented on Jun 1, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    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/cli serve to 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:

    • no Trigger registered: record_change
    • no [record-change] bound flow …
    • no RecordChangeTriggerPlugin: … NOT installed

    So 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 in objectstack start.

    2. Empirical result still: record-change flow never executes

    With everything contract-correct (triggers capability loaded, triggerType: 'record-after-create', condition omitted), inserting a crm_lead via REST does not run the bound flow (next_followup_date never stamped, updated_at unchanged after 30s). Meanwhile object L2 hooks on the same write path do run (lead beforeInsert recomputed rating; an opportunity beforeInsert hook 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)

    • A consumer L2 hook that referenced module-scoped constants from inside the sandboxed handler body threw ReferenceError on 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.
    • With the sqlite-wasm driver, some inserts intermittently fail with COMMIT; - 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.

  2. xuyushun441-sys commented on Jun 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    Still reproduces on @objectstack 7.7.0 (latest). Re-tested against the objectstack-ai/templates all aggregate:

    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 independent record_change flows do not fire on data writes:

    • helpdesk_ai_triage_on_create — create a ticket with status: 'new' → stays new, ai_triage_at null (should advance to triaged).
    • helpdesk_escalate_angry_customer — set ai_sentiment: 'angry' + priority: 'urgent' on a ticket → stays put (should flip to escalated). This one is a clean repro: pure field-equality criteria, a single update_record node, no date comparison, no invoke_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions