crm
Operate HubSpot CRM from a terminal or agent: list and search contact records, view deal pipeline stages, and — with explicit confirmation — move deals between stages, backed by a bundled crm-cli script that is read-only by default and gates every stage change behind a --dry-run/
Install
npx skills add https://github.com/magnus919/agent-skills/tree/main/crm
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
git clone https://github.com/magnus919/agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.
README
CRM — Operate HubSpot from the Terminal
Look up contacts, search records, and view deal pipeline stages from your terminal or agent — and apply confirmed stage changes — all against HubSpot's CRM API.
Why Install This Skill
CRM data is where the answers to "who is this person?" and "what is in the pipeline?" live, and agents have had no bounded way to reach it. This skill gives your agent a real read path into HubSpot (contact records, contact search, deal pipeline views, pipeline stage maps) and a safe write path: moving a deal between stages is a guarded mutation that requires a preview and an explicit confirmation, so the agent can answer sales questions without ever silently changing the pipeline.
It ships crm-cli, a small Python script that speaks the HubSpot CRM v3 API with no third-party dependencies. Reads are capped (--limit), output is clean JSON for the agent or readable text for you, and --help works with no token and no network. Records are summarized as the fields people actually ask about — name, email, company, amount, stage — instead of raw property maps.
What You Get
| Directory | Purpose |
|---|---|
SKILL.md |
Agent-facing operating contract, mutation gates, and verification boundaries |
references/ |
Dated source index and a HubSpot CRM operations reference (endpoints, object model, pagination, stage updates, errors) |
scripts/crm-cli |
Bounded, stdlib-only CLI: contacts list/get/search, deals list/update-stage, pipelines list; --json, --limit, stage changes gated by --dry-run/--yes |
tests/ |
13 deterministic tests against a stub HubSpot API, covering the mutation gate and read-only contract |
evals/evals.json |
Six output-quality evaluation cases for agent runs |
Quick Start
# Help works with no token and no network
crm/scripts/crm-cli --help
# List contacts (capped)
HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json --limit 20 contacts list
# Find a contact by name
HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json contacts search --query "ada"
# View the deal pipeline (optionally filtered)
HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json --limit 20 deals list
HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json deals list --pipeline default --stage appointmentscheduled
# Resolve stage labels to IDs
HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json pipelines list
# Move a deal only with a preview first, then explicit confirmation
HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --dry-run
HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --yes
Triggers
Load this skill for hubspot / crm operations: "who is this contact", searching contacts, what deals are in the pipeline, listing deals by stage, resolving pipeline stages, or moving a deal to a new stage with confirmation. Do not load it for building HubSpot apps or workflow automations, marketing automation, or other CRMs like Salesforce.
Requirements
- Python 3.9+ for
crm-cli(stdlib only;--helpneeds nothing else). - A HubSpot private app access token (
HUBSPOT_TOKEN) with object scopes:crm.objects.contacts.readandcrm.objects.deals.readfor reads, pluscrm.objects.deals.writefor stage updates. - Network access to
api.hubapi.comfor live reads and updates.
Skill manifest
HubSpot CRM Operations
Use this skill to read and, with explicit confirmation, update HubSpot CRM data through the HubSpot CRM v3 API: contact records, contact search, deal pipeline views, and deal stage changes. This is a tool skill for one CRM vendor (HubSpot). Building HubSpot apps or workflows is application development; this skill owns the everyday agent workflow: answering "who is this contact?", "what is in the pipeline?", and applying a confirmed stage change.
Operating contract
- Read-only discovery before any mutation. List and search contacts, view deals and pipelines freely. The bundled
crm-cliscript makes reads without writing anything. - Confirm the target, scope, and rollback path before acting. Moving a deal to a new stage changes a shared pipeline that revenue reporting reads: it requires an explicit human directive naming the deal and the target stage, plus
--dry-runpreview and--yesconfirmation throughcrm-cli. Stage moves are reversible but leave audit history — confirm before acting. - Respect bounded reads. HubSpot pages with
limit; never page past what the task needs.crm-cli --limitcaps every listing and search. - Keep evidence bounded. Quote short names, emails, amounts, and stage labels; never dump full records, tokens, or raw payloads into chat.
- Know the object model. Contacts and deals are distinct objects with property maps; stage transitions must use a stage ID from the deal's pipeline (
pipelines list), not a stage label.
The crm-cli script
scripts/crm-cli is an agent-first, stdlib-only CLI over the HubSpot CRM v3 API. It covers the full issue scope: records, search, and pipeline views.
crm/scripts/crm-cli --help # no token or network needed
crm/scripts/crm-cli --json --limit 20 contacts list
crm/scripts/crm-cli --json contacts get --id 51
crm/scripts/crm-cli --json contacts search --query "ada"
crm/scripts/crm-cli --json --limit 20 deals list
crm/scripts/crm-cli --json deals list --pipeline default --stage appointmentscheduled
crm/scripts/crm-cli --json pipelines list
crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --dry-run # preview
crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --yes # confirmed
Exit codes: 0 success, 1 API error or failed check, 2 usage error. Stage changes are guarded: without --dry-run or --yes the script refuses with exit 1 and never calls the API. Reads are bounded by --limit (default 20, max 100).
Operating loop
- Scope the question: is this a lookup (who/what is in the CRM) or a change (move a deal)? Locate the object with
contacts search/contacts listordeals list. - Read with bounds:
contacts getfor one record,deals listfor the pipeline view (optionally filtered by pipeline and stage),pipelines listto resolve stage labels to IDs. - Triage the answer: map the question to evidence (contact details, deal amount/stage, pipeline distribution).
- Act with confirmation: only a human directive to change, previewed with
--dry-runand confirmed with--yes. - Verify: re-read the deal (
deals list --stage <target>) and confirm the stage moved.
Records, search, pipeline views
- Contact records (
/objects/contacts): list (GET) or retrieve one (GET by ID); the CLI summarizes first/last name, email, company, and created date. Search (POST /objects/contacts/search) finds contacts by query text, bounded by--limit. - Deal pipeline views (
/objects/deals): list deals with amount, pipeline, and stage, optionally filtered to one pipeline or stage.pipelines list(/pipelines/deals) returns the pipelines with their stage IDs and labels — use the stage ID when filtering or updating. - Stage changes (
PATCH /objects/deals/{id}): a guarded mutation that sets thedealstageproperty. Preview the target stage with--dry-run, confirm with--yes, and verify with a follow-up read. Only stage moves are in scope; other deal property edits are application work.
Access model
- HubSpot private app access tokens (
pat_...) scope per object and read/write. Reads needcrm.objects.contacts.readandcrm.objects.deals.read; stage updates needcrm.objects.deals.write. - Records carry a
propertiesmap keyed by property names (e.g.dealstage,dealname,amount). Property values are strings; the CLI summarizes the fields this skill uses. - Tokens are credentials: store in
HUBSPOT_TOKEN, never in code, chat, or commits. Rotate a leaked token in the private app settings.
Reference routing
| Load when | Reference |
|---|---|
| Sources, scope tables, refresh procedure | references/00-source-index.md |
| Endpoints, pagination, object model, stage updates, errors | references/01-hubspot-crm-operations.md |
Included artifacts
scripts/crm-cli: bounded, stdlib-only CLI (contacts list/get/search, deals list/update-stage, pipelines list;--json;--limit; mutations gated by--dry-run/--yes).tests/test_crm_cli.py: 13 deterministic tests against a stub HubSpot API, including the mutation gate and the read-only contract.references/: dated source index + HubSpot CRM operations reference.evals/evals.json: six output-quality evaluation cases for agent runs.
Verification boundary
| Claim | Minimum evidence |
|---|---|
| A contact exists | crm-cli contacts search --query ... --json or contacts get returns the record |
| A pipeline view is accurate | crm-cli deals list --json returns deals with stage IDs and the filter applied |
| A stage label maps to an ID | crm-cli pipelines list --json returns the pipeline stage map |
| A stage change landed | crm-cli deals update-stage --yes exits 0 and a follow-up deals list --stage shows the deal |
| A mutation is safe to run | crm-cli deals update-stage --dry-run prints the exact deal + target stage |
Hard boundaries
- Never move a deal without a human directive,
--dry-runpreview, and--yesconfirmation — pipeline changes feed revenue reporting and audit history. - Never claim a record is missing when the token may lack object scope; check the access model first.
- Never page reads past
--limit; never dump full records, tokens, or raw payloads into chat. - This skill operates the HubSpot CRM API. It does not build HubSpot apps or cover other CRMs.
When not to use
- Building HubSpot apps, workflow automations, or custom objects — that is HubSpot app development; see backend-engineering for service design.
- Marketing, sequences, and email automation in HubSpot — that is the HubSpot Marketing surface, not the CRM API this skill covers.
- Other CRMs (Salesforce, Pipedrive, Zoho) — each has its own API and tooling; this skill covers HubSpot.
- CRM strategy, sales process design, or pipeline methodology — that is organizational/strategy work, not an API operation.
Files (agent-skills)
-
evals
-
evals.json 5 KB
{ "schema_version": 1, "skill_name": "crm", "evals": [ { "id": "contact-lookup", "prompt": "A user asks: 'Find the contact record for Ada Lovelace in HubSpot and show me her email and company.'", "expected_output": "Run crm-cli contacts search --query 'Ada Lovelace' (bounded) and report the matching contact's email, company, and record ID from the summarized properties. If multiple contacts match, list all candidates with IDs and ask the user to disambiguate. Reads only — nothing is changed.", "assertions": [ "Contacts are searched via crm-cli contacts search with a bounded --limit", "The response reports email, company, and record ID for the match", "Multiple candidates are listed for disambiguation instead of picking arbitrarily", "The operation is read-only" ] }, { "id": "pipeline-view", "prompt": "A user asks: 'Show me every deal in the default pipeline that is still in the Appointment Scheduled stage, with amounts.'", "expected_output": "Run crm-cli pipelines list to resolve the pipeline and stage IDs, then crm-cli deals list --pipeline <id> --stage <stage-id> (bounded) and report each deal's name, amount, and stage. The response confirms the filter used and notes the total count. Reads only — nothing is moved.", "assertions": [ "Pipeline and stage IDs are resolved via crm-cli pipelines list before filtering", "Deals are listed with a pipeline and stage filter and a bounded --limit", "Each deal reports name, amount, and stage", "The operation is read-only" ] }, { "id": "stage-update-confirmed", "prompt": "A user asks: 'Move deal 901 (Acme renewal) to Closed Won. It is currently in Appointment Scheduled.'", "expected_output": "The agent states the current stage and the target, previews the change with crm-cli deals update-stage --id 901 --stage closedwon --dry-run, and asks for explicit confirmation. Only after confirmation does it run the update with --yes, then verifies by re-reading the deal's stage. If the user only asked to draft the change, nothing is applied.", "assertions": [ "The exact deal and target stage are previewed with --dry-run before any change", "The current stage and target stage are stated before confirmation", "The update runs only after explicit user confirmation, via --yes", "The result is verified with a follow-up read of the deal stage" ] }, { "id": "search-before-mutation", "prompt": "A user asks: 'Which deals have \"renewal\" in the name, and should we move the biggest one to Closed Won?'", "expected_output": "The agent performs the search as a read: lists deals with renewal-related names via crm-cli deals list and reports names, amounts, and stages, identifying the largest candidate. It does NOT move any deal on its own: a stage change is a guarded mutation requiring explicit confirmation with a --dry-run preview and --yes, so it asks the user to confirm which deal and to which stage before acting.", "assertions": [ "The deal search is performed read-only via crm-cli deals list", "The largest candidate is identified with amount and stage", "No deal is moved without explicit confirmation and a --dry-run preview", "The mutation gate is explained to the user" ] }, { "id": "pipeline-stage-mapping", "prompt": "A user asks: 'What stages exist in our deals pipeline and which one means a deal is won?'", "expected_output": "Run crm-cli pipelines list and report each pipeline with its stages: stage label plus stage ID. The response identifies the won/lost stages by their labels (e.g. Closed Won / Closed Lost) and explains that stage changes use the stage ID, not the label, because the API keys on the ID. Reads only.", "assertions": [ "Pipelines and stages are read via crm-cli pipelines list", "Each stage is reported with both label and ID", "Won/lost stages are identified by label", "The response notes that updates use stage IDs, not labels" ] }, { "id": "access-model-triage", "prompt": "A user asks: 'The API returns a 403 when I list deals. Did we lose our deals?'", "expected_output": "The response distinguishes the failure modes: a 403 from the HubSpot API means the private app token lacks the object scope (crm.objects.deals.read), not that the deals are gone. It advises checking the private app's scopes in the HubSpot settings, re-generating or re-granting the token scope, then re-running crm-cli deals list. It does not claim data loss, does not re-run blindly, and performs no mutations during triage.", "assertions": [ "A 403 is explained as a scope problem rather than data loss", "The fix is checking and re-granting the private app object scopes", "Triage is read-only and no mutation is attempted", "Data loss is only concluded with concrete evidence" ] } ] }
-
-
references
-
00-source-index.md 1.2 KB
# HubSpot CRM — Source Index > **Last Updated:** 2026-08-03 This skill is a distilled operating layer over HubSpot's public developer documentation. Facts and endpoint names in this skill are grounded in the sources below; refresh this index when HubSpot ships API changes. | Topic | Source | URL | |---|---|---| | CRM object model | Understanding the CRM | https://developers.hubspot.com/docs/api/crm/understanding-the-crm | | Contacts API | Contacts | https://developers.hubspot.com/docs/api/crm/contacts | | Deals API | Deals | https://developers.hubspot.com/docs/api/crm/deals | | Deal pipelines API | Pipelines | https://developers.hubspot.com/docs/api/crm/pipelines | | Search API | Search | https://developers.hubspot.com/docs/api/crm/search | | Private apps and scopes | Private apps | https://developers.hubspot.com/docs/api/private-apps | ## Refresh procedure - Re-check the object model when a 403 or `PROPERTY_DOES_NOT_EXIST` appears for a documented property; HubSpot object schemas evolve. - Re-check the pipelines API before changing anything in `deals update-stage`; stage IDs are pipeline-scoped. - Update `research_checked` in `SKILL.md` frontmatter and this file's `Last Updated` when you verify the sources again. -
01-hubspot-crm-operations.md 3.7 KB
# HubSpot CRM Operations > **Last Updated:** 2026-08-03 Operational detail for the HubSpot CRM v3 API surface the skill owns: contact records, contact search, deal pipeline views, and guarded deal stage updates. The bundled `crm-cli` implements this reference; use this document when a call behaves unexpectedly. ## API conventions - Base URL: `https://api.hubapi.com/crm/v3`. Every request carries `Authorization: Bearer <private_app_token>` and JSON bodies. - Access is scope-based: private app tokens grant per-object read/write. A 403 means the token lacks the scope — check `crm.objects.contacts.read`, `crm.objects.deals.read` (reads) and `crm.objects.deals.write` (stage updates) before concluding anything else. - `crm-cli` honors `HUBSPOT_API_BASE` (test/stub override); production default is the v3 base. ## Endpoint surface | Operation | Endpoint | Method | Notes | |---|---|---|---| | List contacts | `/objects/contacts?limit=N` | GET | Summarized as name, email, company, createdate | | Get a contact | `/objects/contacts/{id}` | GET | Single record | | Search contacts | `/objects/contacts/search` | POST | JSON body `{"query": ..., "limit": N}` | | List deals | `/objects/deals?limit=N` (+ `pipeline`, `dealstage`) | GET | Pipeline view with amount + stage | | Move a deal | `/objects/deals/{id}` | PATCH | Guarded mutation: sets the `dealstage` property | | List pipelines | `/pipelines/deals` | GET | Pipelines with stage IDs and labels | ## Object model - Every object is an `id` plus a `properties` map keyed by property name. Property values are strings in list/search responses (e.g. `dealstage`, `dealname`, `amount`, `email`, `firstname`, `lastname`, `company`). - Deals belong to a pipeline (`pipeline` property) and a stage (`dealstage` property) whose valid values come from `/pipelines/deals`. **Stage changes use the stage ID, never the label.** - Search (`POST /objects/contacts/search`) accepts a `query` string and a `limit`; it matches across default contact searchable properties. ## Pagination and bounded reads - List endpoints return `results` plus `total` and `paging.next.after` (offset cursor). `limit` caps per-request results (max 100 for most object APIs). - **Bounded-read rule:** request only what the task needs; `crm-cli --limit` caps at the request level. Report the `total` alongside the returned results so the reader knows the cap hid further records. ## Guarded stage updates - Moving a deal sets `{"properties": {"dealstage": "<stage-id>"}}` via PATCH. Preview with `--dry-run` (prints the exact deal + target stage), confirm with `--yes`, then verify by re-reading the deal. - Stage moves are visible to the whole revenue team and land in audit history. They are reversible, but every move is a recorded change — confirm before acting. - Only the `dealstage` property is in this skill's mutation surface. Other deal property edits are application work. ## Error handling - 401 `unauthorized`: token invalid or revoked — rotate the private app token. - 403 `forbidden`: token lacks object scope — grant the scope in the private app settings, don't retry blindly. - 404 `not found`: object does not exist **or** the token cannot see it — verify object ID and scope before concluding deletion. - 429 `RATE_LIMIT`: slow down; HubSpot rate limits per token. - `crm-cli` exit 1 with `HubSpot API HTTP <code>: <message>` (human) or `{"ok": false, "error": "..."}` (JSON). Exit 2 is a usage error. ## Credential hygiene - Private app tokens are full object-scope credentials: store in `HUBSPOT_TOKEN`, never in code, chat, or commits. Scope tokens to the objects the task needs and rotate on leak. - Personal data (emails, names, amounts) lives in CRM records; quote only what the question needs and never dump full records into chat.
-
-
scripts
-
crm-cli 12.5 KB · in bundle
-
-
tests
-
test_crm_cli.py 9.4 KB
#!/usr/bin/env python3 """Deterministic tests for crm/scripts/crm-cli. Runs the script as a subprocess so the tests exercise the real CLI surface (--help, --json, --limit, mutation gate, exit codes, JSON payloads). A local stdlib HTTP server stubs the HubSpot CRM v3 API (contacts, deals, pipelines, search), so no external network or HubSpot account is needed. Also asserts the read-only contract: reads never call write methods, and the mutation gate refuses to move a deal without --dry-run or --yes. """ import json import os import subprocess import sys import threading import unittest from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SCRIPT = ROOT / "scripts" / "crm-cli" CONTACT = {"id": "51", "properties": {"firstname": "Ada", "lastname": "Lovelace", "email": "ada@example.com", "company": "Analytical", "createdate": "2026-01-01T00:00:00Z"}} DEAL = {"id": "901", "properties": {"dealname": "Acme renewal", "amount": "12000", "pipeline": "default", "dealstage": "appointmentscheduled", "hs_lastmodifieddate": "2026-01-02T00:00:00Z"}} PIPELINE = {"id": "default", "label": "Default pipeline", "stages": [{"id": "appointmentscheduled", "label": "Appointment Scheduled", "displayOrder": 0}]} class StubHubSpotServer: """Minimal stub of the HubSpot CRM v3 API surface used by crm-cli.""" def __init__(self): self.requests = [] # (method, path, body) handler = self._make_handler() self.server = ThreadingHTTPServer(("127.0.0.1", 0), handler) self.port = self.server.server_address[1] self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) def _make_handler(self): stub = self class Handler(BaseHTTPRequestHandler): def _read_body(self): length = int(self.headers.get("Content-Length", "0")) raw = self.rfile.read(length) return json.loads(raw.decode("utf-8")) if raw else {} def _json(self, payload, status=200): self.send_response(status) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps(payload).encode("utf-8")) def do_GET(self): # noqa: N802 stub.requests.append(("GET", self.path, None)) if "/objects/contacts?" in self.path: self._json({"results": [CONTACT], "total": 1, "paging": {}}) elif "/objects/contacts/" in self.path: self._json(CONTACT) elif "/objects/deals?" in self.path: self._json({"results": [DEAL], "total": 1, "paging": {}}) elif "/pipelines/deals" in self.path: self._json({"results": [PIPELINE]}) else: self._json({"message": "not_found"}, 404) def do_POST(self): # noqa: N802 body = self._read_body() stub.requests.append(("POST", self.path, body)) if "/search" in self.path: limit = body.get("limit", 20) self._json({"results": [CONTACT][:limit], "total": 1}) else: self._json({"message": "not_found"}, 404) def do_PATCH(self): # noqa: N802 body = self._read_body() stub.requests.append(("PATCH", self.path, body)) if "/objects/deals/" in self.path: updated = json.loads(json.dumps(DEAL)) updated["properties"]["dealstage"] = body.get("properties", {}).get("dealstage") self._json(updated) else: self._json({"message": "not_found"}, 404) def log_message(self, *args): # silence stderr pass return Handler def __enter__(self): self.thread.start() return self def __exit__(self, *exc): self.server.shutdown() self.server.server_close() def run_script(env, *args): return subprocess.run( [sys.executable, str(SCRIPT), *args], capture_output=True, text=True, timeout=30, env=env, ) def base_env(stub): env = dict(os.environ) env["HUBSPOT_TOKEN"] = "pat-test" env["HUBSPOT_API_BASE"] = f"http://127.0.0.1:{stub.port}/" return env def load_json(proc): return json.loads(proc.stdout) class CrmCliTests(unittest.TestCase): def test_help_lists_json_and_bounded_reads(self): proc = run_script(dict(os.environ), "--help") self.assertEqual(proc.returncode, 0) self.assertIn("--json", proc.stdout) self.assertIn("--limit", proc.stdout) def test_help_works_without_token(self): env = dict(os.environ) env.pop("HUBSPOT_TOKEN", None) proc = run_script(env, "contacts", "list", "--help") self.assertEqual(proc.returncode, 0) def test_contacts_list(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "--json", "--limit", "5", "contacts", "list") self.assertEqual(proc.returncode, 0, proc.stderr) data = load_json(proc) self.assertEqual(data["contacts"][0]["email"], "ada@example.com") def test_contacts_get(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "--json", "contacts", "get", "--id", "51") self.assertEqual(proc.returncode, 0, proc.stderr) self.assertEqual(load_json(proc)["contact"]["id"], "51") def test_contacts_search(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "--json", "contacts", "search", "--query", "ada") self.assertEqual(proc.returncode, 0, proc.stderr) data = load_json(proc) self.assertEqual(data["total"], 1) self.assertEqual(data["contacts"][0]["firstname"], "Ada") def test_search_sends_limit(self): with StubHubSpotServer() as stub: run_script(base_env(stub), "--json", "--limit", "3", "contacts", "search", "--query", "ada") posts = [body for method, path, body in stub.requests if method == "POST" and "/search" in path] self.assertEqual(len(posts), 1) self.assertEqual(posts[0].get("limit"), 3) def test_deals_list_pipeline_view(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "--json", "deals", "list", "--pipeline", "default", "--stage", "appointmentscheduled") self.assertEqual(proc.returncode, 0, proc.stderr) data = load_json(proc) self.assertEqual(data["deals"][0]["dealname"], "Acme renewal") gets = [path for method, path, _ in stub.requests if method == "GET"] self.assertTrue(any("objects/deals?" in path for path in gets)) def test_pipelines_list(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "--json", "pipelines", "list") self.assertEqual(proc.returncode, 0, proc.stderr) data = load_json(proc) self.assertEqual(data["pipelines"][0]["id"], "default") self.assertEqual(data["pipelines"][0]["stages"][0]["id"], "appointmentscheduled") def test_update_stage_requires_confirmation(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "deals", "update-stage", "--id", "901", "--stage", "closedwon") self.assertEqual(proc.returncode, 1) self.assertIn("refusing to move", proc.stderr) self.assertEqual(stub.requests, [], "no API call may be made without confirmation") def test_update_stage_dry_run_does_not_patch(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "--json", "deals", "update-stage", "--id", "901", "--stage", "closedwon", "--dry-run") self.assertEqual(proc.returncode, 0, proc.stderr) data = load_json(proc) self.assertTrue(data["dry_run"]) self.assertEqual(stub.requests, [], "dry-run must not reach the API") def test_update_stage_with_yes_patches(self): with StubHubSpotServer() as stub: proc = run_script(base_env(stub), "--json", "deals", "update-stage", "--id", "901", "--stage", "closedwon", "--yes") self.assertEqual(proc.returncode, 0, proc.stderr) data = load_json(proc) self.assertEqual(data["deal"]["dealstage"], "closedwon") self.assertTrue(any(method == "PATCH" for method, _path, _body in stub.requests)) def test_missing_token_errors_cleanly(self): env = dict(os.environ) env.pop("HUBSPOT_TOKEN", None) proc = run_script(env, "--json", "contacts", "list") self.assertEqual(proc.returncode, 1) self.assertIn("HUBSPOT_TOKEN", proc.stdout) def test_read_only_contract_no_write_opens(self): source = SCRIPT.read_text() writes = [line for line in source.splitlines() if line.strip().startswith("open(") and ("'w'" in line or '"w"' in line)] self.assertEqual(writes, [], "script must never open files in write mode") if __name__ == "__main__": unittest.main()
-
-
README.md 3.3 KB
# CRM — Operate HubSpot from the Terminal Look up contacts, search records, and view deal pipeline stages from your terminal or agent — and apply confirmed stage changes — all against HubSpot's CRM API. ## Why Install This Skill CRM data is where the answers to "who is this person?" and "what is in the pipeline?" live, and agents have had no bounded way to reach it. This skill gives your agent a real read path into HubSpot (contact records, contact search, deal pipeline views, pipeline stage maps) and a safe write path: moving a deal between stages is a guarded mutation that requires a preview and an explicit confirmation, so the agent can answer sales questions without ever silently changing the pipeline. It ships `crm-cli`, a small Python script that speaks the HubSpot CRM v3 API with no third-party dependencies. Reads are capped (`--limit`), output is clean JSON for the agent or readable text for you, and `--help` works with no token and no network. Records are summarized as the fields people actually ask about — name, email, company, amount, stage — instead of raw property maps. ## What You Get | Directory | Purpose | |---|---| | `SKILL.md` | Agent-facing operating contract, mutation gates, and verification boundaries | | `references/` | Dated source index and a HubSpot CRM operations reference (endpoints, object model, pagination, stage updates, errors) | | `scripts/crm-cli` | Bounded, stdlib-only CLI: contacts list/get/search, deals list/update-stage, pipelines list; `--json`, `--limit`, stage changes gated by `--dry-run`/`--yes` | | `tests/` | 13 deterministic tests against a stub HubSpot API, covering the mutation gate and read-only contract | | `evals/evals.json` | Six output-quality evaluation cases for agent runs | ## Quick Start ```bash # Help works with no token and no network crm/scripts/crm-cli --help # List contacts (capped) HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json --limit 20 contacts list # Find a contact by name HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json contacts search --query "ada" # View the deal pipeline (optionally filtered) HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json --limit 20 deals list HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json deals list --pipeline default --stage appointmentscheduled # Resolve stage labels to IDs HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli --json pipelines list # Move a deal only with a preview first, then explicit confirmation HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --dry-run HUBSPOT_TOKEN=pat_... crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --yes ``` ## Triggers Load this skill for `hubspot` / `crm` operations: "who is this contact", searching contacts, what deals are in the pipeline, listing deals by stage, resolving pipeline stages, or moving a deal to a new stage with confirmation. Do not load it for building HubSpot apps or workflow automations, marketing automation, or other CRMs like Salesforce. ## Requirements - Python 3.9+ for `crm-cli` (stdlib only; `--help` needs nothing else). - A HubSpot private app access token (`HUBSPOT_TOKEN`) with object scopes: `crm.objects.contacts.read` and `crm.objects.deals.read` for reads, plus `crm.objects.deals.write` for stage updates. - Network access to `api.hubapi.com` for live reads and updates. -
SKILL.md 8 KB
--- name: crm description: >- Operate HubSpot CRM from a terminal or agent: list and search contact records, view deal pipeline stages, and — with explicit confirmation — move deals between stages, backed by a bundled crm-cli script that is read-only by default and gates every stage change behind a --dry-run/--yes confirmation. Use when an agent needs to answer questions about contacts or deals, produce pipeline views, or apply a confirmed stage change. Do not use for building HubSpot apps or workflow automations (that is HubSpot app development), marketing/sequence automation, or other CRMs like Salesforce (that is their own tooling). license: MIT compatibility: >- The bundled crm-cli script runs on Python 3.9+ with only the standard library. --help and all reads need no network beyond api.hubapi.com; live reads require a HubSpot private app access token with the relevant object scopes (crm.objects.contacts.read, crm.objects.deals.read) and network access to api.hubapi.com. metadata: source: https://developers.hubspot.com/docs/api/crm/understanding-the-crm source_index: references/00-source-index.md research_checked: "2026-08-03" --- # HubSpot CRM Operations Use this skill to read and, with explicit confirmation, update HubSpot CRM data through the HubSpot CRM v3 API: contact records, contact search, deal pipeline views, and deal stage changes. This is a **tool skill** for one CRM vendor (**HubSpot**). Building HubSpot apps or workflows is application development; this skill owns the everyday agent workflow: answering "who is this contact?", "what is in the pipeline?", and applying a confirmed stage change. ## Operating contract 1. **Read-only discovery before any mutation.** List and search contacts, view deals and pipelines freely. The bundled `crm-cli` script makes reads without writing anything. 2. **Confirm the target, scope, and rollback path before acting.** Moving a deal to a new stage changes a shared pipeline that revenue reporting reads: it requires an explicit human directive naming the deal and the target stage, plus `--dry-run` preview and `--yes` confirmation through `crm-cli`. Stage moves are reversible but leave audit history — confirm before acting. 3. **Respect bounded reads.** HubSpot pages with `limit`; never page past what the task needs. `crm-cli --limit` caps every listing and search. 4. **Keep evidence bounded.** Quote short names, emails, amounts, and stage labels; never dump full records, tokens, or raw payloads into chat. 5. **Know the object model.** Contacts and deals are distinct objects with property maps; stage transitions must use a stage ID from the deal's pipeline (`pipelines list`), not a stage label. ## The crm-cli script `scripts/crm-cli` is an agent-first, stdlib-only CLI over the HubSpot CRM v3 API. It covers the full issue scope: records, search, and pipeline views. ```bash crm/scripts/crm-cli --help # no token or network needed crm/scripts/crm-cli --json --limit 20 contacts list crm/scripts/crm-cli --json contacts get --id 51 crm/scripts/crm-cli --json contacts search --query "ada" crm/scripts/crm-cli --json --limit 20 deals list crm/scripts/crm-cli --json deals list --pipeline default --stage appointmentscheduled crm/scripts/crm-cli --json pipelines list crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --dry-run # preview crm/scripts/crm-cli deals update-stage --id 901 --stage closedwon --yes # confirmed ``` Exit codes: 0 success, 1 API error or failed check, 2 usage error. Stage changes are guarded: without `--dry-run` or `--yes` the script refuses with exit 1 and never calls the API. Reads are bounded by `--limit` (default 20, max 100). ## Operating loop 1. **Scope the question**: is this a lookup (who/what is in the CRM) or a change (move a deal)? Locate the object with `contacts search`/`contacts list` or `deals list`. 2. **Read with bounds**: `contacts get` for one record, `deals list` for the pipeline view (optionally filtered by pipeline and stage), `pipelines list` to resolve stage labels to IDs. 3. **Triage the answer**: map the question to evidence (contact details, deal amount/stage, pipeline distribution). 4. **Act with confirmation**: only a human directive to change, previewed with `--dry-run` and confirmed with `--yes`. 5. **Verify**: re-read the deal (`deals list --stage <target>`) and confirm the stage moved. ## Records, search, pipeline views - **Contact records** (`/objects/contacts`): list (GET) or retrieve one (GET by ID); the CLI summarizes first/last name, email, company, and created date. Search (`POST /objects/contacts/search`) finds contacts by query text, bounded by `--limit`. - **Deal pipeline views** (`/objects/deals`): list deals with amount, pipeline, and stage, optionally filtered to one pipeline or stage. `pipelines list` (`/pipelines/deals`) returns the pipelines with their stage IDs and labels — use the stage ID when filtering or updating. - **Stage changes** (`PATCH /objects/deals/{id}`): a guarded mutation that sets the `dealstage` property. Preview the target stage with `--dry-run`, confirm with `--yes`, and verify with a follow-up read. Only stage moves are in scope; other deal property edits are application work. ## Access model - HubSpot private app access tokens (`pat_...`) scope per object and read/write. Reads need `crm.objects.contacts.read` and `crm.objects.deals.read`; stage updates need `crm.objects.deals.write`. - Records carry a `properties` map keyed by property names (e.g. `dealstage`, `dealname`, `amount`). Property values are strings; the CLI summarizes the fields this skill uses. - Tokens are credentials: store in `HUBSPOT_TOKEN`, never in code, chat, or commits. Rotate a leaked token in the private app settings. ## Reference routing | Load when | Reference | |---|---| | Sources, scope tables, refresh procedure | `references/00-source-index.md` | | Endpoints, pagination, object model, stage updates, errors | `references/01-hubspot-crm-operations.md` | ## Included artifacts - `scripts/crm-cli`: bounded, stdlib-only CLI (contacts list/get/search, deals list/update-stage, pipelines list; `--json`; `--limit`; mutations gated by `--dry-run`/`--yes`). - `tests/test_crm_cli.py`: 13 deterministic tests against a stub HubSpot API, including the mutation gate and the read-only contract. - `references/`: dated source index + HubSpot CRM operations reference. - `evals/evals.json`: six output-quality evaluation cases for agent runs. ## Verification boundary | Claim | Minimum evidence | |---|---| | A contact exists | `crm-cli contacts search --query ... --json` or `contacts get` returns the record | | A pipeline view is accurate | `crm-cli deals list --json` returns deals with stage IDs and the filter applied | | A stage label maps to an ID | `crm-cli pipelines list --json` returns the pipeline stage map | | A stage change landed | `crm-cli deals update-stage --yes` exits 0 and a follow-up `deals list --stage` shows the deal | | A mutation is safe to run | `crm-cli deals update-stage --dry-run` prints the exact deal + target stage | ## Hard boundaries - Never move a deal without a human directive, `--dry-run` preview, and `--yes` confirmation — pipeline changes feed revenue reporting and audit history. - Never claim a record is missing when the token may lack object scope; check the access model first. - Never page reads past `--limit`; never dump full records, tokens, or raw payloads into chat. - This skill operates the HubSpot CRM API. It does not build HubSpot apps or cover other CRMs. ## When not to use - **Building HubSpot apps, workflow automations, or custom objects** — that is HubSpot app development; see [backend-engineering](../backend-engineering/SKILL.md) for service design. - **Marketing, sequences, and email automation in HubSpot** — that is the HubSpot Marketing surface, not the CRM API this skill covers. - **Other CRMs** (Salesforce, Pipedrive, Zoho) — each has its own API and tooling; this skill covers HubSpot. - **CRM strategy, sales process design, or pipeline methodology** — that is organizational/strategy work, not an API operation.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.