Flag HubSpot deals with missing fields and Slack the rep using a Claude Code skill

low complexityCost: Usage-based
Compatible agents

This skill works with any agent that supports the Claude Code skills standard, including Claude Code, Claude Cowork, OpenAI Codex, and Google Antigravity.

Prerequisites
  • One of the agents listed above
  • HubSpot private app with crm.objects.deals.read scope
  • Slack bot with chat:write permission, added to the target channel or able to DM users
Environment Variables
# HubSpot private app token (Settings > Integrations > Private Apps)
HUBSPOT_ACCESS_TOKEN=your_value_here
# Your HubSpot portal ID (used to build direct links to deal records)
HUBSPOT_PORTAL_ID=your_value_here
# Slack bot token starting with xoxb- (chat:write scope required)
SLACK_BOT_TOKEN=your_value_here

Why a Claude Code skill?

The other approaches in this guide run on a fixed daily schedule. An Claude Code skill is different. You tell Claude what you want in plain language, and the skill gives it enough context to do it reliably.

That means you can say:

  • "Check for deals with missing fields and DM the owners"
  • "Which deals in the Proposal stage are missing close dates?"
  • "Audit just deals owned by Sarah for missing amounts"

The skill contains workflow guidelines, API reference materials, and a Slack message template that the agent reads on demand. When you invoke the skill, Claude reads these files, writes a script on the fly, runs it, and reports results. If you ask for something different — a specific stage, a different set of required fields, or a channel post instead of DMs — the agent adapts without you touching any code.

How it works

The skill directory has three parts:

  1. SKILL.md — workflow guidelines telling the agent what steps to follow, which env vars to use, and what pitfalls to avoid
  2. references/ — HubSpot API patterns (deal search with NOT_HAS_PROPERTY, owners endpoint) so the agent calls the right APIs
  3. templates/ — a Slack Block Kit template so missing field alerts are consistently formatted across runs

When invoked, the agent reads SKILL.md, consults the reference and template files as needed, writes a Python script, executes it, and reports what it found. The reference files act as guardrails — the agent knows exactly which endpoints to hit, what filter operators to use, and how the responses are structured.

What is a Claude Code skill?

An Claude Code skill is a reusable command you add to your project that Claude Code can run on demand. Skills live in a .claude/skills/ directory and are defined by a SKILL.md file that tells the agent what the skill does, when to run it, and what tools it's allowed to use.

In this skill, the agent doesn't run a pre-written script. Instead, SKILL.md provides workflow guidelines and points to reference files — API documentation, message templates — that the agent reads to generate and execute code itself. This is the key difference from a traditional script: the agent can adapt its approach based on what you ask for while still using the right APIs and message formats.

Once installed, you can invoke a skill as a slash command (e.g., /missing-deal-fields), or the agent will use it automatically when you give it a task where the skill is relevant. Skills are portable — anyone who clones your repo gets the same commands.

Step 1: Create the skill directory

mkdir -p .claude/skills/missing-deal-fields/{templates,references}

This creates the layout:

.claude/skills/missing-deal-fields/
├── SKILL.md                                # workflow guidelines + config
├── templates/
│   └── slack-missing-fields-alert.md      # Block Kit template for Slack DMs
└── references/
    └── hubspot-deals-search-api.md        # HubSpot API patterns for deal field auditing

Step 2: Write the SKILL.md

Create .claude/skills/missing-deal-fields/SKILL.md:

---
name: missing-deal-fields
description: Find HubSpot deals missing required fields (close date, amount) and DM each deal owner in Slack with a list of deals to fix
disable-model-invocation: true
allowed-tools: Bash, Read
---
 
## Goal
 
Search for active deals in HubSpot that are missing required fields, group them by owner, and send each owner a Slack DM listing their incomplete deals with direct links to fix them.
 
## Configuration
 
Read these environment variables:
 
- `HUBSPOT_ACCESS_TOKEN` — HubSpot private app token (required)
- `HUBSPOT_PORTAL_ID` — HubSpot portal ID for building deal links (required)
- `SLACK_BOT_TOKEN` — Slack bot token starting with xoxb- (required)
 
Default required fields: `closedate`, `amount`. The user may request additional fields.
 
Owner-to-Slack mapping: The agent needs a way to map HubSpot owner IDs to Slack user IDs. Check for an `owner_map.json` file in the skill directory first. If not found, fetch the Slack users list and HubSpot owners list and match by email address.
 
## Workflow
 
1. Validate that all required env vars are set. If any are missing, print which ones and exit.
2. For each required field, search HubSpot for active deals where that field has not been set. See `references/hubspot-deals-search-api.md` for the endpoint and filter syntax.
3. Deduplicate deals across searches (a deal missing both close date and amount appears in both results). Track which specific fields each deal is missing.
4. Group deals by `hubspot_owner_id`.
5. Resolve owner IDs to Slack user IDs using owner_map.json or by matching HubSpot owner emails to Slack user emails.
6. Send each owner a Slack DM using the format in `templates/slack-missing-fields-alert.md`.
7. Print a summary: total deals flagged, owners notified, and any owners without a Slack mapping.
 
## Important notes
 
- HubSpot's Search API applies AND logic within a filter group. You CANNOT combine multiple `NOT_HAS_PROPERTY` filters with OR in a single query. Run one search per field and merge results.
- `NOT_HAS_PROPERTY` only matches properties that were never set. If a field was set and then cleared, HubSpot may store it as an empty string. Consider also searching for `EQ` with empty string value.
- Exclude closed deals (`dealstage NOT_IN closedwon, closedlost`) from the search.
- Deal links follow the format: `https://app.hubspot.com/contacts/PORTAL_ID/deal/DEAL_ID`
- Slack user IDs start with U (not W). The bot must have chat:write scope to DM users.
- Use the `requests` library for HTTP calls and `slack_sdk` for Slack. Install them with pip if needed.

Understanding the SKILL.md

Unlike a script-based skill, this SKILL.md doesn't contain a Run: command pointing to a script. Instead, it provides:

SectionPurpose
GoalTells the agent what outcome to produce
ConfigurationWhich env vars to read and how to handle owner mapping
WorkflowNumbered steps with pointers to reference files
Important notesNon-obvious context — especially the AND-only filter limitation

The allowed-tools: Bash, Read setting lets the agent both read reference files and execute code. The agent writes its own script based on the workflow steps and reference materials.

Step 3: Add reference files

templates/slack-missing-fields-alert.md

Create .claude/skills/missing-deal-fields/templates/slack-missing-fields-alert.md:

# Slack Missing Fields Alert Template
 
Use this Block Kit structure when DMing a deal owner about missing fields.
 
## Block Kit JSON
 
```json
{
  "channel": "<SLACK_USER_ID>",
  "text": "Missing deal fields: <count> deals",
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "🔍 Missing Deal Fields (<count> deals)"
      }
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "<deal_lines>"
      }
    },
    {
      "type": "context",
      "elements": [
        {
          "type": "mrkdwn",
          "text": "Please update these deals today so forecasting stays accurate."
        }
      ]
    }
  ]
}
```
 
## Deal line format
 
Each deal line: `- <https://app.hubspot.com/contacts/PORTAL_ID/deal/DEAL_ID|Deal Name> — missing *close date, amount*`
 
## Notes
 
- The top-level `text` field is required by Slack as a fallback for notifications.
- Bold the missing field names with asterisks (Slack mrkdwn).
- If a deal is missing multiple fields, join them with commas.
- Use singular "1 deal" when count is 1.
- Sort deals alphabetically by name within each owner's message.

references/hubspot-deals-search-api.md

Create .claude/skills/missing-deal-fields/references/hubspot-deals-search-api.md:

# HubSpot Deals Search API Reference — Missing Fields
 
## Search for deals missing a property
 
Use `NOT_HAS_PROPERTY` to find deals where a field has never been set.
 
**Request:**
 
```
POST https://api.hubapi.com/crm/v3/objects/deals/search
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
 
**Body (missing close date):**
 
```json
{
  "filterGroups": [
    {
      "filters": [
        {
          "propertyName": "closedate",
          "operator": "NOT_HAS_PROPERTY"
        },
        {
          "propertyName": "dealstage",
          "operator": "NOT_IN",
          "values": ["closedwon", "closedlost"]
        }
      ]
    }
  ],
  "properties": ["dealname", "amount", "closedate", "dealstage", "hubspot_owner_id"],
  "limit": 100
}
```
 
**Body (missing amount):**
 
Same structure, but swap `closedate` for `amount` in the `NOT_HAS_PROPERTY` filter.
 
### Important: AND-only logic
 
HubSpot's Search API applies AND logic within a filter group. You CANNOT combine `closedate NOT_HAS_PROPERTY OR amount NOT_HAS_PROPERTY` in a single query. Run one search per field and merge results client-side.
 
### Catching cleared fields
 
`NOT_HAS_PROPERTY` only matches properties that were never set. If a rep entered a value and then cleared it, HubSpot may store an empty string. To catch both cases, add a second filter group:
 
```json
{
  "filterGroups": [
    {
      "filters": [
        { "propertyName": "closedate", "operator": "NOT_HAS_PROPERTY" },
        { "propertyName": "dealstage", "operator": "NOT_IN", "values": ["closedwon", "closedlost"] }
      ]
    },
    {
      "filters": [
        { "propertyName": "closedate", "operator": "EQ", "value": "" },
        { "propertyName": "dealstage", "operator": "NOT_IN", "values": ["closedwon", "closedlost"] }
      ]
    }
  ],
  "properties": ["dealname", "amount", "closedate", "dealstage", "hubspot_owner_id"],
  "limit": 100
}
```
 
Multiple filter groups are combined with OR logic.
 
**Response shape:**
 
```json
{
  "total": 8,
  "results": [
    {
      "id": "12345",
      "properties": {
        "dealname": "Acme Corp Expansion",
        "amount": null,
        "closedate": null,
        "dealstage": "qualifiedtobuy",
        "hubspot_owner_id": "67890"
      }
    }
  ],
  "paging": {
    "next": { "after": "100" }
  }
}
```
 
- `limit` max is 100. Use `paging.next.after` to paginate.
- Properties that have never been set return `null`.
 
## Get deal owners
 
Map HubSpot owner IDs to names and emails (for matching to Slack users).
 
**Request:**
 
```
GET https://api.hubapi.com/crm/v3/owners
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
```
 
**Response shape:**
 
```json
{
  "results": [
    {
      "id": "67890",
      "email": "jane@company.com",
      "firstName": "Jane",
      "lastName": "Smith"
    }
  ]
}
```
 
Use the owner's email to match against Slack users, or maintain a static `owner_map.json` mapping owner IDs to Slack user IDs.

Step 4: Add the owner mapping (optional)

Create .claude/skills/missing-deal-fields/owner_map.json:

{
  "12345678": "U0XXXXXXXXX",
  "87654321": "U0YYYYYYYYY"
}

Replace the keys with your HubSpot owner IDs and the values with the corresponding Slack user IDs. If you skip this file, the agent will attempt to match owners to Slack users by email address.

Step 5: Test the skill

Invoke the skill conversationally:

/missing-deal-fields

Claude will read the SKILL.md, check the reference files, write a script, install any missing dependencies, run it, and report the results. A typical run looks like:

Searching for deals missing close date... found 6
Searching for deals missing amount... found 4
After deduplication: 8 deals with missing fields
 
Grouped by owner:
  Jane Smith (3 deals): Acme Corp Expansion, Globex New Business, Initech Platform Deal
  Bob Jones (2 deals): Umbrella Renewal, Stark Industries Upgrade
  Unassigned (3 deals): skipped (no Slack mapping)
 
Sending Slack DMs...
  Notified Jane Smith about 3 deals
  Notified Bob Jones about 2 deals
 
Done. 8 deals flagged, 2 owners notified, 3 deals skipped (unassigned).

What the Slack DM looks like

What you'll get
#direct-message
Deal Hygiene Botapp9:41 AM

🔍 Missing Deal Fields (3 deals)

Acme Corp Expansion — missing close date, amount

Globex New Business — missing next step

Initech Platform Deal — missing amount

Please update these deals today so forecasting stays accurate.

Because the agent generates code on the fly, you can also make ad hoc requests:

  • "Check just deals in the Proposal stage" — the agent adds a stage filter
  • "Audit deals for close date, amount, and next step" — the agent adds another field
  • "Post the audit results to #sales-pipeline instead of DMs" — the agent changes the Slack target
Test with real data

Make sure you have at least a few open deals in your pipeline with missing fields. If all deals are complete, the skill correctly reports "All active deals have complete fields" — that's not an error.

Step 6: Schedule it (optional)

Option A: Cron + Claude CLI

# Run every weekday at 7 AM
0 7 * * 1-5 cd /path/to/your/project && claude -p "Run /missing-deal-fields" --allowedTools 'Bash(*)' 'Read(*)'

Option B: GitHub Actions + Claude

name: Daily Missing Deal Fields Audit
on:
  schedule:
    - cron: '0 12 * * 1-5'  # 7 AM ET = 12 PM UTC, weekdays only
  workflow_dispatch: {}
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
        with:
          prompt: "Run /missing-deal-fields"
          allowed_tools: "Bash(*),Read(*)"
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
          HUBSPOT_PORTAL_ID: ${{ secrets.HUBSPOT_PORTAL_ID }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

Option C: Cowork Scheduled Tasks

Claude Desktop's Cowork supports built-in scheduled tasks. Open a Cowork session, type /schedule, and configure the cadence — hourly, daily, weekly, or weekdays only. Each scheduled run has full access to your connected tools, plugins, and MCP servers.

Scheduled tasks only run while your computer is awake and Claude Desktop is open. If a run is missed, Cowork executes it automatically when the app reopens. For always-on scheduling, use GitHub Actions (Option B) instead. Available on all paid plans (Pro, Max, Team, Enterprise).

GitHub Actions cron uses UTC

0 12 * * 1-5 runs at 12 PM UTC (7 AM ET) on weekdays. GitHub Actions cron may also have up to 15 minutes of delay.

Troubleshooting

When to use this approach

  • You want on-demand data audits — before pipeline reviews, after data imports, or during weekly cleanup
  • You want conversational flexibility — different required fields, specific stages, or custom filters
  • You want to iterate on what fields matter without editing scripts
  • You're already using Claude Code and want deal hygiene as a quick command
  • You want to run tasks in the background via Claude Cowork while focusing on other work

When to switch approaches

  • You need reliable daily alerts with zero manual intervention → use n8n or the code approach
  • You want a no-code setup with a visual builder → use Make
  • You need alerts running 24/7 with zero cost and no LLM usage → use the Code + Cron approach

Common questions

Why not just use a script?

A script checks the same fields the same way every time. The Claude Code skill adapts — you can ask for different fields, filter by stage or owner, or post to a channel instead of DMs. The reference files ensure it calls the right APIs and uses the right filter operators, even when improvising.

Does this use Claude API credits?

Yes. The agent reads skill files and generates code each time. Typical cost is $0.01-0.05 per invocation. The HubSpot and Slack APIs themselves are free.

Can I check custom HubSpot properties?

Yes. Tell the agent which fields to check — it reads the reference file to understand the filter syntax and applies NOT_HAS_PROPERTY to whatever fields you specify. The SKILL.md defaults to close date and amount, but the agent adapts to any deal property.

How does the owner-to-Slack mapping work?

The agent first checks for an owner_map.json file in the skill directory. If found, it uses that. If not, it can match HubSpot owner emails to Slack user emails (requires users:read scope on the Slack bot). You can also just tell the agent to post everything to a shared channel instead.

Cost

  • Claude API — $0.01-0.05 per invocation (the agent reads files and generates code)
  • HubSpot API — included in all plans, no per-call cost
  • Slack API — included in all plans, no per-call cost
  • GitHub Actions (if scheduled) — free tier includes 2,000 minutes/month

Looking to scale your AI operations?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.