Article
Detecting Hidden Workflow Cycles: Building a HubSpot DAG Analyzer with Claude Code
Most RevOps teams discover a workflow loop only after a contact record gets stuck in a recursive execution cascade. You see the signs in the property history first: a single field flipping between two values 50 times in three minutes. By then, your sync queue is backed up, your webhook limits are peaking, and your data integrity is already compromised.
HubSpot has native guardrails like enrollment limits and throttling. These prevent a total platform meltdown, but they don't fix the underlying logic error. HubSpot’s internal validator catches simple self-enrollment loops within a single workflow, but it rarely flags cross-workflow dependencies. If Workflow A triggers on Property X and sets Property Y, and Workflow B triggers on Property Y and sets Property X, you have a loop that HubSpot treats as two valid, independent processes.
We can solve this by treating automations as a system of nodes and edges. By using Claude Code to build a local static analyzer, we can parse workflow definitions and identify these cycles before they ever reach a production portal.
The Data: Accessing Workflow Definitions
To analyze the system, you need the blueprint. The HubSpot Workflows API (/automation/v3/workflows) allows you to export the full JSON definition of your automation suite.
When inspecting the JSON for a workflow, two keys matter for dependency mapping:
enrollmentCriteria: This contains the filters that trigger the workflow. We need to extract every property name mentioned in these filters.actions: This is an array of steps. We specifically look for actions where thetypeisSET_CONTACT_PROPERTYorSET_COMPANY_PROPERTY. These represent the "outputs" of the workflow.
In a complex portal with hundreds of workflows, manually mapping these triggers is impossible. This is a classic Directed Acyclic Graph (DAG) problem. In a functional GTM system, property updates should flow in one direction. A cycle is, by definition, a logic bug.
Building the Analyzer with Claude Code
I used Claude Code to scaffold a Python utility that handles the heavy lifting. The goal was a script that reads a directory of HubSpot JSON exports and identifies any path that leads back to its starting point.
You can initialize this in your terminal with a prompt like:
"Write a Python script to read a directory of HubSpot workflow JSON files. Extract the workflow name, the properties used in enrollment triggers, and the properties updated by 'SET_CONTACT_PROPERTY' actions. Build a directed graph using the NetworkX library where an edge exists from Workflow A to Workflow B if A updates a property that B triggers on. Use the simple_cycles function to flag circular dependencies."
Claude Code produces a structure that maps these relationships. Here is the core logic for the parser:
import json
import networkx as nx
import os
def parse_workflows(directory):
workflow_nodes = []
for filename in os.listdir(directory):
if filename.endswith('.json'):
with open(os.path.join(directory, filename)) as f:
data = json.load(f)
workflow_nodes.append({
'name': data['name'],
'triggers': extract_trigger_props(data['enrollmentCriteria']),
'updates': extract_update_props(data['actions'])
})
return workflow_nodes
def find_cycles(workflow_data):
G = nx.DiGraph()
for wf in workflow_data:
G.add_node(wf['name'])
for wf_a in workflow_data:
for wf_b in workflow_data:
# Edge exists if A sets a property that B listens to
common = set(wf_a['updates']).intersection(set(wf_b['triggers']))
if common:
G.add_edge(wf_a['name'], wf_b['name'], properties=list(common))
return list(nx.simple_cycles(G))
This script turns a pile of JSON into a mathematical model. If nx.simple_cycles returns anything, your automation logic is fundamentally recursive.
Visualizing the Logic Collisions
Finding the cycle is the first step; explaining it to a stakeholder is the second. You can prompt Claude Code to take the identified cycles and output them in Mermaid.js syntax for documentation.
For example, a three-way loop involving Lead Scoring, Persona Tagging, and a Lifecycle Stage router becomes immediately clear when visualized:
graph TD
A[Lead Score Calculator] -->|Score| B[Persona Tagger]
B -->|Persona| C[Lifecycle Stage Router]
C -->|Lifecycle Stage| A
Seeing the loop visually makes the fix obvious—usually introducing a "breaker" property or adjusting re-enrollment settings to prevent the cycle from repeating.
The Cost of Logic Collisions
Why treat RevOps like engineering? Because the operational failure modes of unmanaged cycles are expensive:
- Property Value Thrashing: If two workflows fight over a "Lead Status" value, your reporting becomes useless. One workflow sets it to "Nurture" and the other immediately flips it back to "MQL," creating a blur of activity that masks real buyer intent.
- Audit Trail Bloat: Every execution adds a line to the record's property history. A few hours of an undetected loop can add millions of rows to your logs, making it impossible to troubleshoot legitimate issues because the signal-to-noise ratio is destroyed.
- Webhook Cascades: If these workflows trigger outbound webhooks to an enrichment service or Slack, a loop becomes a self-inflicted DDoS attack on your own infrastructure or a quick way to burn through your API credits.
Known Unknowns in Static Analysis
Static analysis is powerful but has blind spots. This script cannot easily see:
- Custom Code Actions: Unless you perform deep analysis of the Python/Node.js snippets within HubSpot, the script won't know which properties are modified by a
hubspot_client.crm.contacts.basic_api.updatecall. - External Integrations: If Zapier or Make listens to a HubSpot webhook and updates a property in response, that represents an "invisible" edge in your graph.
- Conditional Branching: The script flags potential cycles, but if a workflow has complex internal logic (If/Then branches), the cycle might only trigger under specific, rare conditions.
Integrating Analysis into Change Management
This shouldn't be a tool you run only when things break. It belongs in the RevOps change management process.
Before deploying a new set of complex automations, export the proposed JSON and run it through your analyzer. This is effectively unit testing for GTM operations. It shifts the team from a reactive posture—fixing loops after they've trashed your data—to a proactive one where you prove the logic is sound before the first record ever enrolls.
— C.B.