Automations
Loop Protection

Loop Protection

Because automation actions can cascade — adding a tag fires a tag_applied trigger, which can add another tag, and so on — Gordon CRM includes built-in safety mechanisms to prevent infinite loops.

The Problem

Consider this set of rules:

RuleTriggerAction
Rule ATag "Lead" appliedAdd tag "Prospect"
Rule BTag "Prospect" appliedAdd tag "Lead"

Without protection, this would create an infinite loop:

Tag "Lead" applied
  → Add "Prospect"
    → Tag "Prospect" applied
      → Add "Lead"
        → Tag "Lead" applied
          → Add "Prospect"
            → … (forever)

Gordon CRM prevents this with two complementary strategies.


Strategy 1: Depth Limiter

The processAutomationTrigger function accepts an internal depth parameter (default 0). Every time an action causes a cascading trigger, the depth is incremented:

processAutomationTrigger(…, depth = 0)     ← Original trigger
  → add_tag action → fire tag_applied
     processAutomationTrigger(…, depth = 1)  ← First cascade
       → add_tag action → fire tag_applied
          processAutomationTrigger(…, depth = 2)  ← Second cascade
            → add_tag action → fire tag_applied
               processAutomationTrigger(…, depth = 3)  ← Third cascade
                 → add_tag action → fire tag_applied
                    processAutomationTrigger(…, depth = 4)  ← ABORT

The hard limit is depth > 3. When the depth exceeds 3, the engine stops additional cascading to safeguard system stability.

This limit allows for legitimate chaining (up to 3 levels deep) while preventing runaway loops.


Strategy 2: Idempotent Operations

Even without the depth limiter, the tag operations themselves provide a natural circuit breaker:

Add Tag (Idempotent)

The add_tag action uses an upsert with ON CONFLICT (contact_id, tag_id) DO NOTHING:

INSERT INTO contact_tags (contact_id, tag_id)
VALUES ($1, $2)
ON CONFLICT (contact_id, tag_id) DO NOTHING;

If the contact already has the tag, the operation succeeds silently but no row is changed. The engine checks the result — if no rows were affected (the tag was already present), it does not fire the cascading tag_applied trigger. This naturally breaks the loop because the duplicate tag application produces no side effect.

Remove Tag (Idempotent)

The remove_tag action deletes the row and checks the SELECT return value. If no row was deleted (the tag was already absent), the cascading tag_removed trigger is not fired.


How The Two Strategies Work Together

ScenarioStrategy 1 (Depth)Strategy 2 (Idempotency)
A → B → A (circular)Would stop at depth 4Stops earlier — A is already applied, so the second application is a no-op
A → B → C → D → E (long chain)Stops at depth 4Does not apply — each tag is different
A → A (self-referencing)Would stop at depth 4Stops immediately — A is already applied

In practice, idempotency catches most circular loops before the depth limiter kicks in. The depth limiter acts as a safety net for edge cases where a long chain of different tags might trigger each other in sequence.


Campaign Enrollment Protection

The enroll_campaign action has its own independent idempotency check:

  • If the contact already has an active or processing enrollment → skip (no duplicate enrollment)
  • If a triggerEventId is present and a completed enrollment exists for the same trigger event → skip (prevents webhook retries from re-enrolling)

These checks prevent a scenario where cascading tag automations could enroll the same contact into a campaign multiple times.


Best Practices

  1. Keep chains short. Design your automations to resolve within 1–2 cascade levels when possible.
  2. Avoid circular tag dependencies. If "Tag A applied → Add Tag B" exists, avoid creating "Tag B applied → Add Tag A".
  3. Use campaign enrollment wisely. Campaign enrollment is naturally idempotent, but long chains that lead to enrollment still consume processing time.
  4. Test your automation chains. If a contact is not receiving a tag or isn't being enrolled in a campaign as expected, check if your logic has created a cycle that is hitting the depth safety limit.