Skip to content

fix(platform): retire automation triggers of a deleted organization - #3025

Open
larryro wants to merge 1 commit into
mainfrom
fix/org-delete-retires-automation-triggers
Open

fix(platform): retire automation triggers of a deleted organization#3025
larryro wants to merge 1 commit into
mainfrom
fix/org-delete-retires-automation-triggers

Conversation

@larryro

@larryro larryro commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Deleting an organization cascades a lot — audit log, personalization hard-delete, the org's config directory, memberMirror — but automationTriggers rows were never disabled or deleted. A surviving enabled: true schedule keeps coming due on the minutely scan (crons.tsscanScheduledTriggers), and each started run crashes deep in the agent host's throwing slug resolver (agent_host.ts orgSlugFromId): one uncaught OrgSlugUnresolvableError per scheduled occurrence, forever, plus the wasted runs. Unlike the #3019 incident this needs no returning visitor — it self-sustains. The incident's org happened to have no schedule triggers, which is the only reason this gap is still latent.

Fixes #3022.

What changed

Two layers, so a missed cascade can't error forever:

  1. Delete cascade (convex/auth.ts + convex/automations/triggers.ts): the /organization/delete after-hook now calls the new internal.automations.triggers.cascadeDeleteOrgTriggers right next to cascadeDeleteOrgMembersMirror — i.e. only after Better Auth actually deleted the org row, so a failed deletion keeps a live org's schedules and webhook tokens intact. Deleting rather than disabling matches the platform's hard-delete posture for org-owned rows (memberMirror, personalization); the mutation is idempotent and indexed (by_org). It has its own catch + warn so a failure is neither mislabeled as a mirror failure nor fatal to the hook.
  2. Scan-side retirement (scanScheduledTriggers): a due trigger now resolves its org via orgSlugFromIdOrNull (the same graceful pattern the crawler uses in knowledge/crawl_action.ts); if the org no longer resolves, the trigger is patched enabled: false with a warn and never starts a run. This retires the orphans that pre-date the cascade, and anything a failed hook leaves behind. The lookup runs only for due triggers (not per scan row), so the steady-state cost of the minutely scan is unchanged; a transient lookup failure still throws, and the scan simply retries next tick.

Webhook and event paths need no scan-side guard: their trigger rows die with the cascade, and a deleted row already yields the token-secrecy 404.

Tests

convex/automations/triggers.test.ts:

  • new: a due schedule whose org is gone is examined but not fired, its row flips to enabled: false, no run row exists, and the next scan examines nothing;
  • new: the cascade deletes exactly the deleted org's triggers (schedule + webhook), leaves the other org's intact, and is idempotent;
  • new: after the cascade, the dead org's webhook token gets the indistinguishable 404 and starts nothing;
  • updated: the two existing scan tests now seed real betterAuth organization rows (components.betterAuth.adapter.create, the arena_action.test.ts pattern) — required because a schedule that should fire now needs an org that resolves; the fixture org names deliberately don't.

Full convex/automations/ suite: 236 passed (14 files); convex/auth.test.ts 7 passed.

Not in scope (follow-up candidates)

Other org-scoped tables (automationVersions, automationRuns, websites, …) still keep orphan rows after an org deletion — inert without a trigger, but worth an orphan-row policy audit as noted in #3022.

Related: #3019 (the incident this was split from), sibling PRs for #3019/#3020/#3021.

Gate: tsc --noEmit clean, oxlint --type-aware clean, oxfmt, vitest 243 passed (automations + auth).

Deleting an organization cascaded the audit log, personalization data,
config directory and memberMirror — but never its automationTriggers
rows. A surviving enabled schedule keeps coming due on the minutely
scan, and each run crashes in the agent host's throwing orgSlugFromId:
one uncaught OrgSlugUnresolvableError per occurrence, forever, with no
returning visitor required.

Two layers: the /organization/delete after-hook now deletes the org's
trigger rows next to the member-mirror cascade (only after Better Auth
confirmed the deletion), and the schedule scan retires any due trigger
whose organization no longer resolves — the backstop for orgs deleted
before the cascade existed or a hook that failed mid-flight.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug service: platform Platform service

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: deleting an organization leaves its automation schedule triggers enabled — the minutely scan then crashes in orgSlugFromId forever

1 participant