Skip to content

fix: register workflow triggers on apply#162

Merged
michaelmcnees merged 2 commits into
mainfrom
claude/office-assistant-mantle-feasibility-n8f4bk
Jul 2, 2026
Merged

fix: register workflow triggers on apply#162
michaelmcnees merged 2 commits into
mainfrom
claude/office-assistant-mantle-feasibility-n8f4bk

Conversation

@michaelmcnees

@michaelmcnees michaelmcnees commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Cron, webhook, and email triggers declared in a workflow's YAML were parsed and validated but never fired. The only function that populated the workflow_triggers table (server.SyncTriggers) had zero callers, so the table stayed empty — and the cron scheduler, webhook handler, and email poller all read from it. This makes trigger registration actually happen on apply.

Changes

  • workflow.Save reconciles workflow_triggers in the same transaction as the definition insert, so both mantle apply and GitOps sync register a workflow's triggers atomically with each new version.
  • workflow.Disable/Reenable toggle the triggers' enabled flag. The schedulers key off workflow_triggers.enabled (not the definition's disabled_at), so a pruned/disabled workflow would otherwise keep firing. Reenable also restores triggers for the case where a pruned workflow reappears with byte-identical content (where Save is a no-op).
  • The email poller periodically reconciles against the DB (every 30s) so email triggers registered by a later mantle apply (a separate process) or a GitOps sync activate without a server restart. Cron and webhook triggers are already read live per tick/request, so they need no equivalent.
  • Removed the dead server.SyncTriggers/TriggerInput; the trigger-write logic now lives in the workflow package (avoids an import cycle, since server already imports workflow).

Testing

  • make test passes — unit tests (cron matching, email poller start/stop/reload) pass locally; the new integration tests are testcontainer-based and require Docker, which was unavailable in the dev sandbox (the pre-existing TestSave_* tests hit the same limitation). They should run on CI.
  • make lintgo build ./... and go vet pass; golangci-lint not run locally.
  • New tests added for new functionality — internal/workflow/triggers_test.go covers registration on apply, version replacement (no duplicate accumulation), the no-trigger case, and the disable/reenable enabled-toggle.

Related Issues

Closes #160


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Workflow triggers are now saved alongside workflow updates, so scheduled, webhook, and email triggers stay in sync automatically.
    • New email triggers can become active without restarting the server.
  • Bug Fixes

    • Disabled workflows now have their triggers turned off, preventing them from firing unexpectedly.
    • Re-enabling a workflow restores its triggers cleanly.
    • Updating a workflow now replaces old triggers instead of creating duplicates.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@michaelmcnees, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 731e7e7a-65ae-472d-b28e-fe55d1b7f265

📥 Commits

Reviewing files that changed from the base of the PR and between d058c26 and b5da9b0.

📒 Files selected for processing (6)
  • .changeset/fix-trigger-registration.md
  • packages/engine/internal/server/email_trigger.go
  • packages/engine/internal/server/trigger.go
  • packages/engine/internal/workflow/store.go
  • packages/engine/internal/workflow/triggers.go
  • packages/engine/internal/workflow/triggers_test.go
📝 Walkthrough

Walkthrough

Workflow trigger reconciliation is moved from an unused server-side SyncTriggers function into a new workflow.syncTriggersTx, invoked transactionally within workflow.Save, Disable, and Reenable. The email trigger poller now periodically reloads configuration. Tests and a changeset document the fix.

Changes

Trigger registration fix

Layer / File(s) Summary
Remove unused SyncTriggers
packages/engine/internal/server/trigger.go
Deletes the never-called SyncTriggers function, TriggerInput type, nullableString helper, and fmt import; adds a comment pointing to the new workflow-package logic.
New transactional trigger sync
packages/engine/internal/workflow/triggers.go
Adds syncTriggersTx, which deletes and re-inserts workflow_triggers rows for a workflow within a given transaction, plus nullableString for optional fields.
Wire sync into Save/Disable/Reenable
packages/engine/internal/workflow/store.go
Save inserts the workflow definition and reconciles triggers in one transaction; Disable and Reenable toggle workflow_triggers.enabled alongside workflow status updates, all transactional.
Periodic email trigger reload
packages/engine/internal/server/email_trigger.go
Adds emailReloadInterval and a ticker-driven goroutine in Start that periodically calls Reload, so new email triggers activate without a restart.
Regression tests and changeset
packages/engine/internal/workflow/triggers_test.go, .changeset/fix-trigger-registration.md
Adds tests validating trigger registration, replacement, empty-trigger handling, and Disable/Reenable toggling, plus a changeset describing the fix.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WorkflowStore as workflow.Save
  participant DB as Database (Tx)
  participant EmailPoller as EmailTriggerPoller

  Client->>WorkflowStore: Save(workflow YAML)
  WorkflowStore->>DB: BEGIN transaction
  WorkflowStore->>DB: INSERT workflow_definitions
  WorkflowStore->>DB: syncTriggersTx (DELETE + INSERT workflow_triggers)
  WorkflowStore->>DB: COMMIT
  DB-->>WorkflowStore: success

  loop every emailReloadInterval
    EmailPoller->>DB: Reload triggers
    DB-->>EmailPoller: updated trigger set
  end
Loading

Poem

A trigger once slept, forgotten, unbound,
Till a rabbit dug deep and the fix was found.
Now cron, mail, and hooks wake with Save,
Triggers enabled, no restart, so brave! 🐇
Thump-thump goes my heart, the workflows now run,
Every thirty seconds, a new one begun~ ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: workflow triggers are registered when workflows are applied.
Linked Issues check ✅ Passed The PR reconciles declared triggers into workflow_triggers during workflow.Save, reloads email triggers periodically, and adds regression tests.
Out of Scope Changes check ✅ Passed The changes stay focused on trigger persistence, reload behavior, and related tests, with no unrelated code paths introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/office-assistant-mantle-feasibility-n8f4bk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d058c26979

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Register the workflow's declared triggers so the cron/webhook/email
// schedulers actually fire it. Done in the same transaction as the
// definition insert so version and triggers commit atomically.
if err := syncTriggersTx(ctx, tx, teamID, name, newVersion, result.Workflow.Triggers); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile triggers for unchanged applies

Because Save still returns 0, nil when latestHash == hash before reaching this new reconciliation call, re-applying an already-saved workflow after upgrading to this fix will not backfill workflow_triggers. That is the common recovery path for workflows created before this change, where workflow_definitions already has the matching hash but the trigger table is empty, so cron/webhook/email triggers remain inert until the user makes a byte-level YAML change.

Useful? React with 👍 / 👎.

Comment on lines +31 to +33
`INSERT INTO workflow_triggers
(workflow_name, workflow_version, type, schedule, path, secret, team_id, mailbox, folder, filter, poll_interval)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope trigger uniqueness by team before inserting

This insert now makes applies fail in multi-tenant deployments when another team already registered the same webhook path, or the same workflow name and cron schedule, because the existing unique indexes on workflow_triggers are not team-scoped (path for webhooks and (workflow_name, schedule) for cron). Since the rest of the trigger read paths carry/use team_id, two teams should be able to apply the same workflow or webhook path under their own auth context; the new registration path needs a migration to include team_id in those unique constraints before relying on this insert.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/engine/internal/workflow/store.go (1)

32-34: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Backfill triggers on the unchanged-content Save path.

latestHash == hash returns before syncTriggersTx, so workflows applied before this fix can still have zero workflow_triggers rows after a no-op mantle apply/GitOps sync. Reconcile the latest version’s triggers on this path too, preserving disabled state if applicable, and add a regression test that seeds a definition without trigger rows then applies identical content.

Also applies to: 61-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/internal/workflow/store.go` around lines 32 - 34, The
unchanged-content branch in `Store.Save` returns before `syncTriggersTx`, which
skips trigger reconciliation for existing workflows with stale or missing rows.
Update the `Save`/`syncTriggersTx` flow so the no-op hash path still reconciles
the latest version’s triggers, while preserving disabled state when present, and
add a regression test that seeds a definition without `workflow_triggers` rows
and then applies identical content to verify the rows are backfilled.
🧹 Nitpick comments (1)
packages/engine/internal/workflow/triggers_test.go (1)

47-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing field-level assertions for the webhook trigger.

The test verifies cron (schedule) and email (mailbox, folder, filter, poll_interval) field persistence, but the webhook trigger's path (/incoming) and secret (shh) values declared in the YAML are never asserted — only the total row count (3) is checked. Since this is the regression test for the bug where trigger data silently failed to persist, verifying the webhook column mapping would close a real coverage gap analogous to the email trigger assertions already present.

✅ Suggested addition
+	// The webhook trigger row must carry its path and secret.
+	var path, secret string
+	if err := database.QueryRowContext(ctx,
+		`SELECT COALESCE(path,''), COALESCE(secret,'') FROM workflow_triggers
+		 WHERE workflow_name = 'triggered-wf' AND type = 'webhook'`,
+	).Scan(&path, &secret); err != nil {
+		t.Fatalf("querying webhook trigger: %v", err)
+	}
+	if path != "/incoming" || secret != "shh" {
+		t.Errorf("webhook fields = (%q,%q), want (/incoming,shh)", path, secret)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/engine/internal/workflow/triggers_test.go` around lines 47 - 100,
The regression test in TestSave_RegistersTriggers covers cron and email trigger
persistence but misses webhook field-level validation. Add assertions for the
webhook row in workflow_triggers, using the existing Save, countTriggers, and
database.QueryRowContext checks to verify the webhook-specific path and secret
values from triggeredWorkflowYAML persist correctly alongside the row count.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/engine/internal/workflow/store.go`:
- Around line 32-34: The unchanged-content branch in `Store.Save` returns before
`syncTriggersTx`, which skips trigger reconciliation for existing workflows with
stale or missing rows. Update the `Save`/`syncTriggersTx` flow so the no-op hash
path still reconciles the latest version’s triggers, while preserving disabled
state when present, and add a regression test that seeds a definition without
`workflow_triggers` rows and then applies identical content to verify the rows
are backfilled.

---

Nitpick comments:
In `@packages/engine/internal/workflow/triggers_test.go`:
- Around line 47-100: The regression test in TestSave_RegistersTriggers covers
cron and email trigger persistence but misses webhook field-level validation.
Add assertions for the webhook row in workflow_triggers, using the existing
Save, countTriggers, and database.QueryRowContext checks to verify the
webhook-specific path and secret values from triggeredWorkflowYAML persist
correctly alongside the row count.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 47c5f431-ff11-466e-a588-e6d1adc255f6

📥 Commits

Reviewing files that changed from the base of the PR and between c8e74a0 and d058c26.

📒 Files selected for processing (6)
  • .changeset/fix-trigger-registration.md
  • packages/engine/internal/server/email_trigger.go
  • packages/engine/internal/server/trigger.go
  • packages/engine/internal/workflow/store.go
  • packages/engine/internal/workflow/triggers.go
  • packages/engine/internal/workflow/triggers_test.go

Copy link
Copy Markdown
Collaborator Author

Thanks for the reviews. Status on the feedback:

✅ Backfill on unchanged re-apply (Codex P1 / CodeRabbit Major) — fixed in 8b3339f. Save now reconciles triggers on the unchanged-hash path too, so a workflow applied before this fix (definition present, workflow_triggers empty) registers its triggers on a plain re-apply — no byte-level YAML change needed. Reconciliation only runs when the stored rows have actually drifted from the latest version's declared triggers, so steady-state GitOps re-applies stay churn-free (important because the email poller keys off trigger IDs). Backfilled rows inherit the workflow's enabled/disabled state so a disabled workflow doesn't start firing. Added regression tests for backfill, no-churn-on-repeat, and disabled-state preservation.

✅ Webhook field assertions (CodeRabbit nitpick) — added path/secret assertions to TestSave_RegistersTriggers in 8b3339f.

⏭️ Team-scoped trigger uniqueness (Codex P2) — valid, but deferring to a separate change. The unique indexes (path for webhooks; (workflow_name, schedule) for cron) aren't team-scoped. For webhooks this can't be fixed by the index alone: the inbound /hooks/<path> handler resolves team via auth.TeamIDFromContext, which is always the default team for unauthenticated webhooks — so webhook routing isn't team-aware today and making the path team-unique would need a broader routing change. The cron case is a genuine latent multi-tenant collision, but a proper fix needs a migration plus a decision on the webhook routing model, which is beyond this bug's scope. Single-tenant (the default) is unaffected. I'll file a follow-up issue for the multi-tenant trigger-uniqueness work.

CI

  • Govulncheck — pre-existing failure unrelated to this PR: four newly-published advisories in existing deps (pgx, docker/docker) that govulncheck@latest picks up against the live DB; main is affected too. Fixed separately in fix(ci): resolve new govulncheck advisories (pgx bump + docker exclusions) #163 (bump pgx → v5.9.2 for the real SQL-injection advisory; allowlist the three unfixable docker/docker ones). Once fix(ci): resolve new govulncheck advisories (pgx bump + docker exclusions) #163 merges to main, this check goes green here on rebase.
  • Test — the failure was two unrelated flakes (TestEmailMove_MovesMessage: IMAP test-server EOF; TestPoller_TicksAndStops: temp-dir cleanup race), both in packages this PR doesn't touch. The changed packages (internal/workflow, internal/server) passed.

Generated by Claude Code

claude added 2 commits July 2, 2026 01:50
Cron, webhook, and email triggers declared in a workflow's YAML were
parsed and validated but never written to workflow_triggers, because the
only function that populated that table (server.SyncTriggers) had no
callers. The cron scheduler, webhook handler, and email poller all read
from that table, so declared triggers never fired.

- workflow.Save now reconciles workflow_triggers in the same transaction
  as the definition insert, so both `mantle apply` and GitOps sync
  register a workflow's triggers atomically with its new version.
- workflow.Disable/Reenable toggle the triggers' enabled flag so a
  pruned/disabled workflow stops firing (schedulers key off
  workflow_triggers.enabled, not the definition's disabled_at) and a
  re-enabled unchanged workflow gets its triggers back.
- The email poller periodically reconciles against the DB so email
  triggers registered by a later apply (a separate process) or a GitOps
  sync activate without a server restart. Cron and webhook triggers are
  already read live per tick/request.
- Removed the dead server.SyncTriggers/TriggerInput; trigger-write logic
  now lives in the workflow package (avoids an import cycle).

Adds integration tests covering registration, version replacement, the
no-trigger case, and the disable/reenable toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnKgCRfMvFcZAkoVbr8k79
Addresses PR review feedback on the trigger-registration fix.

- workflow.Save reconciles triggers on the unchanged-content path too, so
  workflows applied before this fix (definition present, workflow_triggers
  empty) register their triggers on a plain re-apply instead of requiring
  a byte-level YAML change. Reconciliation only runs when the stored rows
  have actually drifted from the latest version's declared triggers, so
  steady-state GitOps re-applies don't churn rows (which matters for the
  email poller, whose IMAP connections key off trigger IDs).
- Backfilled rows inherit the workflow's enabled/disabled state, so a
  disabled workflow doesn't start firing on re-apply.
- Tests: assert webhook path/secret persistence, add backfill regression
  (including no-churn on repeat apply) and disabled-state preservation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnKgCRfMvFcZAkoVbr8k79
@michaelmcnees michaelmcnees force-pushed the claude/office-assistant-mantle-feasibility-n8f4bk branch from 8b3339f to b5da9b0 Compare July 2, 2026 01:50
@michaelmcnees michaelmcnees merged commit dbb5acd into main Jul 2, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Workflow triggers declared in YAML are never activated — SyncTriggers has no callers

2 participants