hubspot-lifecycle-and-lists
Manage HubSpot lifecycle stages and list segmentation in production without silently destroying CRM trust. Covers lifecycle stage progression guards that prevent regression, dynamic list criteria drift, static list orphan detection, lead-scoring source-of-truth conflicts, webhook-missed-event recovery, and cross-portal list sync. Use when moving contacts through the funnel, building segment-based nurture flows, auditing list membership integrity, reconciling external lead scores with HubSpot native scoring, or standing up a webhook consumer that must never lose a membership-change event. Trigger with "hubspot lifecycle", "hubspot list segmentation", "hubspot dynamic list", "hubspot static list", "hubspot lead scoring conflict", "lifecycle regression", "list membership webhook", "cross-portal list sync".
What this skill does
# HubSpot Lifecycle and Lists
## Overview
Move contacts through the HubSpot funnel and maintain list integrity in a production system. This is not a setup walkthrough — it is the code your marketing-ops integration runs when a lifecycle stage update would silently regress a Customer back to Subscriber, when a dynamic list's criteria change orphans members who qualified last week, when a static list import references contacts that were deleted from the portal, when an external lead-scoring model creates a second source of truth that fights HubSpot's native scoring, when a list-membership webhook fires but your consumer returns 5xx and HubSpot never retries, and when an agency needs to mirror list membership across two portals that have no native sync API.
The six production failures this skill prevents:
1. **Lifecycle stage regression** — HubSpot's `PATCH /crm/v3/objects/contacts/{id}` will set lifecyclestage to any valid value regardless of direction. Setting a Customer back to Subscriber silently destroys funnel attribution, invalidates reporting, and corrupts revenue forecasting. The API returns `200 OK`. There is no built-in guard.
2. **Dynamic list criteria drift** — editing a dynamic list's filter criteria does not immediately re-evaluate existing members against the new rules. Members who no longer qualify remain in the list until the nightly background refresh completes, creating a stale membership window of up to 24 hours that can trigger incorrect nurture emails or suppression failures.
3. **Static list import orphans** — contacts added to a static list via import or `POST /contacts/v1/lists/{listId}/add` remain list members even after the underlying contact record is hard-deleted from the CRM. These orphan IDs return errors on any subsequent contact-level API call and pollute downstream sync pipelines.
4. **Lead scoring model disagreement** — writing an external score to a custom contact property while HubSpot's native Lead Scoring tool computes its own score creates two competing signals. Sales works from the HubSpot Score field; marketing automation triggers on the custom property. The two scores diverge and nobody knows which one to trust.
5. **List-membership webhook missed events** — HubSpot's webhook system delivers `contact.propertyChange` events for lifecyclestage updates and list-membership changes via HTTP POST with no retry on 5xx responses. A single downstream outage during a bulk-import window can drop hundreds of membership events permanently with no dead-letter queue or re-delivery mechanism.
6. **Cross-portal list sync** — agencies managing multiple HubSpot portals (e.g., a staging portal mirroring a production portal, or two franchisee portals needing shared suppression lists) have no native API to sync list membership between portals. Manual export-import lags by hours and has no idempotency guarantee.
## Prerequisites
- HubSpot account with a private app token scoped to:
- `crm.objects.contacts.read`
- `crm.objects.contacts.write`
- `crm.lists.read`
- `crm.lists.write`
- `crm.schemas.contacts.read` (for lifecycle stage enumeration)
- Python 3.10+ or Node.js 18+ for implementation examples
- `jq` on PATH for shell-level inspection
- `curl` for API verification steps
- For webhook consumption: an HTTPS endpoint reachable by HubSpot's webhook delivery infrastructure
- For cross-portal sync: private app tokens for both portals stored in separate environment variables or a credential router (see `hubspot-auth` skill)
Store all tokens in a secret manager. Never put `pat-na1-*` values in source code or committed `.env` files.
## Instructions
Build in this order. Each section neutralizes one production failure mode.
### 1. Lifecycle stage progression guard (neutralizes regression)
The lifecycle stage enum has a defined forward direction. Any update that moves a contact backward is almost certainly a data pipeline bug, not an intentional business action.
Canonical stage order (HubSpot internal values, not display labels):
```
subscriber → lead → marketingqualifiedlead → salesqualifiedlead → opportunity → customer → evangelist → other
```
`other` is a lateral bucket, not a terminal stage — it sits outside the linear progression and should only be set explicitly.
Never read the stage order from display labels. Display labels are portal-configurable. Always use the internal enum values.
**Progression guard pattern (Python):**
```python
STAGE_ORDER = [
"subscriber",
"lead",
"marketingqualifiedlead",
"salesqualifiedlead",
"opportunity",
"customer",
"evangelist",
]
# 'other' is not in the linear sequence — treat as a lateral assignment
def stage_index(stage: str) -> int:
try:
return STAGE_ORDER.index(stage.lower())
except ValueError:
return -1 # 'other' or unknown — always allow
def safe_set_lifecycle(contact_id: str, new_stage: str, token: str) -> dict:
current = get_contact_lifecycle(contact_id, token)
current_idx = stage_index(current)
new_idx = stage_index(new_stage)
# Allow lateral 'other' assignments and forward progressions
if current_idx != -1 and new_idx != -1 and new_idx < current_idx:
raise ValueError(
f"Lifecycle regression blocked: {current} → {new_stage} "
f"for contact {contact_id}. "
f"Current index={current_idx}, requested index={new_idx}."
)
return patch_contact_lifecycle(contact_id, new_stage, token)
def get_contact_lifecycle(contact_id: str, token: str) -> str:
import urllib.request, json
url = f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}?properties=lifecyclestage"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
return data["properties"].get("lifecyclestage", "subscriber")
def patch_contact_lifecycle(contact_id: str, stage: str, token: str) -> dict:
import urllib.request, json
url = f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}"
payload = json.dumps({"properties": {"lifecyclestage": stage}}).encode()
req = urllib.request.Request(
url,
data=payload,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
method="PATCH",
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
```
Verify a contact's current stage before running a bulk update:
```bash
curl -s \
"https://api.hubapi.com/crm/v3/objects/contacts/{contact-id}?properties=lifecyclestage" \
-H "Authorization: Bearer {your-token}" \
| jq '.properties.lifecyclestage'
```
### 2. Dynamic list criteria drift (neutralizes stale membership)
When you edit a dynamic list's filter criteria, HubSpot queues a background re-evaluation job. Existing members who no longer satisfy the new criteria remain in the list until that job completes — up to 24 hours. Any automation triggered on list membership during that window operates on stale data.
**Production pattern: snapshot membership before and after a criteria change, then audit the delta.**
```python
import urllib.request, json, time
def snapshot_list_members(list_id: int, token: str) -> set[str]:
"""Return the full set of vid (legacy contact ID) strings for a list."""
vids = set()
offset = None
while True:
url = f"https://api.hubapi.com/contacts/v1/lists/{list_id}/contacts/all?count=100"
if offset:
url += f"&vidOffset={offset}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
for contact in data.get("contacts", []):
vids.add(str(contact["vid"]))
if not data.get("has-more", False):
break
offset = data["vid-offset"]
return Related in Ads & Marketing
ads
IncludedMulti-platform paid advertising audit and optimization skill. Analyzes Google, Meta, YouTube, LinkedIn, TikTok, Microsoft, and Apple Ads. 250+ checks with scoring, parallel agents, industry templates, and AI creative generation.
banana
IncludedAI image generation Creative Director powered by Google Gemini Nano Banana models. Use this skill for ANY request involving image creation, editing, visual asset production, or creative direction. Triggers on: generate an image, create a photo, edit this picture, design a logo, make a banner, visual for my anything, and all /banana commands. Handles text-to-image, image editing, multi-turn creative sessions, batch workflows, and brand presets.
rpg-migration-analyzer
IncludedAnalyzes legacy RPG (Report Program Generator) programs from AS/400 and IBM i systems for migration to modern Java applications. Extracts business logic from RPG III/IV/ILE source code, identifies data structures (D-specs), file operations (F-specs), program dependencies (CALLB/CALLP), and converts RPG constructs to Java equivalents. Generates migration reports, complexity estimates, and Java implementation strategies with POJO classes, JPA entities, and service methods. Use when modernizing AS/400 or IBM i legacy systems, analyzing RPG source files (.rpg, .rpgle, .RPGLE), converting RPG to Java, mapping data specifications to Java classes, planning legacy system migration, or when user mentions RPG analysis, Report Program Generator, RPG III/IV/ILE, AS/400 modernization, IBM i migration, packed decimal conversion, or mainframe application rewrite.
brand-library-architect
IncludedBuild a complete brand library for a product — visual asset render pipeline, brand documentation set (BRAND, COPY, MANIFESTO, BIOS, FAQ, GLOSSARY, TONE, PRICING), open-source convention files (README, CONTRIBUTING, SECURITY, CODE_OF_CONDUCT), and a self-contained press kit. This skill should be used when the user asks to "build a brand library / brand kit / press kit / brand assets" for a product, "set up a brand library workflow," "create a positioning manifesto plus visual identity," or any combination of brand documentation + visual asset pipeline. Apply phase-by-phase or run end-to-end. Templates are product-agnostic and use {{TOKEN}} placeholders the skill prompts the user to fill.
writing-tech-post
IncludedAuthors engineering blog posts end-to-end: launch deep-dives, incident postmortems, architecture migrations, performance case studies, tutorials, AI/agent system writeups, security disclosures, and research-to-product translations. Picks the correct archetype, plans the abstraction ladder, enforces an evidence cadence (diagrams, benchmarks, profiles, traces, code, ablations), tunes voice against publisher house styles (Datadog, Vercel, GitHub, AWS, Meta, Cloudflare, Jane Street), and runs a pre-publish gate for narrative momentum and disclosure ethics. Use when drafting a new engineering post, restructuring a draft that feels flat, deciding which evidence form belongs where, validating that depth and product context are balanced, or preparing a postmortem, migration, or performance narrative for external publication. Do not use for API reference documentation, README authoring, marketing copy, release notes, generic SEO content, ghost-written executive thought leadership, or non-engineering long-form essays.
blog-google
IncludedGoogle API integration for blog performance: PageSpeed Insights, CrUX Core Web Vitals with 25-week history, Search Console performance, URL Inspection, Indexing API, GA4 organic traffic, NLP entity analysis for E-E-A-T, YouTube video search for embedding, and Google Ads Keyword Planner. Progressive feature availability based on credential tier (API key, OAuth/service account, GA4, Ads). Shares config with claude-seo at ~/.config/claude-seo/google-api.json. Use when user says "google data", "page speed", "core web vitals", "search console", "indexation", "GA4", "keyword research", "nlp entities", "blog performance", "youtube search", "google api setup".