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:
| Rule | Trigger | Action |
|---|---|---|
| Rule A | Tag "Lead" applied | Add tag "Prospect" |
| Rule B | Tag "Prospect" applied | Add 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) ← ABORTThe 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
| Scenario | Strategy 1 (Depth) | Strategy 2 (Idempotency) |
|---|---|---|
| A → B → A (circular) | Would stop at depth 4 | Stops earlier — A is already applied, so the second application is a no-op |
| A → B → C → D → E (long chain) | Stops at depth 4 | Does not apply — each tag is different |
| A → A (self-referencing) | Would stop at depth 4 | Stops 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
activeorprocessingenrollment → skip (no duplicate enrollment) - If a
triggerEventIdis present and acompletedenrollment 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
- Keep chains short. Design your automations to resolve within 1–2 cascade levels when possible.
- Avoid circular tag dependencies. If "Tag A applied → Add Tag B" exists, avoid creating "Tag B applied → Add Tag A".
- Use campaign enrollment wisely. Campaign enrollment is naturally idempotent, but long chains that lead to enrollment still consume processing time.
- 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.