Automate a sales-to-CS handoff when a HubSpot deal closes won using a Claude Code skill
Install this skill
Download the skill archive and extract it into your .claude/skills/ directory.
cs-handoff.skill.zipPrerequisites
This skill works with any agent that supports the Claude Code skills standard, including Claude Code, Claude Cowork, OpenAI Codex, and Google Antigravity.
- One of the agents listed above
- HubSpot private app with
crm.objects.deals.read,crm.objects.contacts.read, andcrm.objects.tasks.writescopes - Slack bot with
chat:writepermission added to the target channel
Why a Claude Code skill?
The other approaches in this guide are deterministic: they run the same logic every time, the same way. 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:
- "Process any closed-won deals from today and set up CS handoffs"
- "Check for closed-won deals in the last 48 hours"
- "Post a handoff summary for the Meridian Logistics deal only"
- "What deals closed this week that haven't been handed off yet?"
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 next time — a different lookback window, a specific deal, a summary format — the agent adapts without you touching any code.
How it works
The skill directory has three parts:
SKILL.md— workflow guidelines telling the agent what steps to follow, which env vars to use, and what pitfalls to avoidreferences/— HubSpot API patterns (deal search, contact lookup, task creation) so the agent calls the right APIs with the right parameterstemplates/— a Slack Block Kit template so handoff messages 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 posted.
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., /cs-handoff), or the agent will use it automatically when you give it a task where the skill is relevant.
Step 1: Create the skill directory
mkdir -p .claude/skills/cs-handoff/{templates,references}This creates the layout:
.claude/skills/cs-handoff/
├── SKILL.md # workflow guidelines + config
├── templates/
│ └── slack-handoff.md # Block Kit template for handoff messages
└── references/
└── hubspot-deals-api.md # HubSpot API patternsStep 2: Write the SKILL.md
Create .claude/skills/cs-handoff/SKILL.md:
---
name: cs-handoff
description: Process recent closed-won deals from HubSpot, send handoff notifications to the CS Slack channel, and create onboarding tasks in HubSpot.
disable-model-invocation: true
allowed-tools: Bash, Read
---
## Goal
Find HubSpot deals that recently moved to Closed Won, post a handoff message to Slack with deal context and contact information, and create a high-priority onboarding task in HubSpot for the CS rep.
## Configuration
Read these environment variables:
- `HUBSPOT_ACCESS_TOKEN` — HubSpot private app token (required)
- `SLACK_BOT_TOKEN` — Slack bot token starting with xoxb- (required)
- `SLACK_CHANNEL_ID` — Slack channel ID starting with C (required)
- `CS_OWNER_ID` — HubSpot owner ID for CS task assignment (optional)
Default lookback window: 24 hours. The user may request a different window.
## Workflow
1. Validate that all required env vars are set. If any are missing, print which ones and exit.
2. Search for closed-won deals modified in the lookback window using the HubSpot CRM Search API. See `references/hubspot-deals-api.md` for the request and response format.
3. Track processed deal IDs in a local JSON file to avoid duplicate handoffs.
4. For each new deal:
a. Fetch associated contacts via the deals API.
b. Get the primary contact's details (name, email, phone, title).
c. Resolve the deal owner's name from the owner ID.
5. Post a handoff message to Slack using the template in `templates/slack-handoff.md`.
6. If `CS_OWNER_ID` is set, create a high-priority onboarding task in HubSpot associated to the deal.
7. Print a summary of handoffs processed.
## Important notes
- HubSpot's `hs_lastmodifieddate` updates for any property change, not just stage changes. Some results may not be actual Closed Won transitions. Filter on `dealstage == closedwon` in the search.
- The HubSpot Search API `value` for date filters must be a Unix timestamp in **milliseconds** (multiply seconds by 1000).
- `SLACK_CHANNEL_ID` must be the channel ID (starts with `C`), not the channel name.
- The Slack bot must be invited to the target channel.
- Task association type ID for deal-to-task is `204` (HUBSPOT_DEFINED).
- Use the `requests` library for HubSpot and `slack_sdk` for Slack. Install with pip if needed.Step 3: Add reference files
templates/slack-handoff.md
Create .claude/skills/cs-handoff/templates/slack-handoff.md:
# CS Handoff Slack Template
Use this Block Kit structure for each closed-won deal handoff.
## Block Kit JSON
```json
{
"channel": "<SLACK_CHANNEL_ID>",
"text": "CS Handoff: <deal_name>",
"unfurl_links": false,
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "New Closed-Won Deal — CS Handoff"
}
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Deal*\n<deal_name>" },
{ "type": "mrkdwn", "text": "*Value*\n$<amount>" },
{ "type": "mrkdwn", "text": "*Sales Rep*\n<owner_name>" },
{ "type": "mrkdwn", "text": "*Close Date*\n<close_date>" }
]
},
{ "type": "divider" },
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Primary Contact*\n<firstname> <lastname> (<jobtitle>)\n<email>\n<phone>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Contract Length*\n<contract_length>"
}
}
]
}
```
## Notes
- If the deal has a `description` (sales notes), add it as an additional section block.
- Format amount with commas and no decimal places for readability.
- Truncate the close date to YYYY-MM-DD format.
- Set `unfurl_links: false` to keep the message clean.references/hubspot-deals-api.md
Create .claude/skills/cs-handoff/references/hubspot-deals-api.md:
# HubSpot Deals API Reference
## Authentication
All requests use Bearer token authentication:
```
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
## Search for closed-won deals
**Request:**
```
POST https://api.hubapi.com/crm/v3/objects/deals/search
```
**Body:**
```json
{
"filterGroups": [
{
"filters": [
{
"propertyName": "dealstage",
"operator": "EQ",
"value": "closedwon"
},
{
"propertyName": "hs_lastmodifieddate",
"operator": "GTE",
"value": "<cutoff_timestamp_ms>"
}
]
}
],
"properties": ["dealname", "amount", "closedate", "hubspot_owner_id", "contract_length", "description"],
"limit": 100
}
```
- `value` for date filters is a Unix timestamp in **milliseconds**.
- `limit` max is 100. Paginate using `paging.next.after` if needed.
**Response shape:**
```json
{
"total": 2,
"results": [
{
"id": "12345",
"properties": {
"dealname": "Meridian Logistics",
"amount": "96000",
"closedate": "2026-03-05T00:00:00.000Z",
"hubspot_owner_id": "67890",
"contract_length": "24 months",
"description": "Custom API integration required. Go-live target: Apr 1."
}
}
]
}
```
## Get deal with associations
**Request:**
```
GET https://api.hubapi.com/crm/v3/objects/deals/{deal_id}?associations=contacts
```
**Response associations shape:**
```json
{
"associations": {
"contacts": {
"results": [
{ "id": "55001" }
]
}
}
}
```
## Get a contact
**Request:**
```
GET https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}?properties=firstname,lastname,email,phone,jobtitle,company
```
## Get a deal owner
**Request:**
```
GET https://api.hubapi.com/crm/v3/owners/{owner_id}
```
**Response:**
```json
{
"firstName": "James",
"lastName": "Okoro"
}
```
## Create a task
**Request:**
```
POST https://api.hubapi.com/crm/v3/objects/tasks
```
**Body:**
```json
{
"properties": {
"hs_task_subject": "Onboarding: <deal_name>",
"hs_task_body": "New closed-won deal ready for CS onboarding.\n\nDeal: ...\nValue: ...",
"hs_task_status": "NOT_STARTED",
"hs_task_priority": "HIGH",
"hubspot_owner_id": "<CS_OWNER_ID>"
},
"associations": [
{
"to": { "id": "<deal_id>" },
"types": [
{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 204 }
]
}
]
}
```
- `associationTypeId: 204` is the standard deal-to-task association.
- Requires `crm.objects.tasks.write` scope on the private app.Step 4: Test the skill
Invoke the skill conversationally:
/cs-handoffClaude will read the SKILL.md, check the reference files, write a script, install dependencies, run it, and report results. A typical run looks like:
Searching for closed-won deals in the last 24 hours...
Found 3 closed-won deals, 2 new
Processing: Meridian Logistics
Slack: posted handoff message
HubSpot: created onboarding task (ID: 78901)
Processing: Widget Corp Expansion
Slack: posted handoff message
HubSpot: CS_OWNER_ID not set — skipping task
Done. Processed 2 handoff(s).Because the agent generates code on the fly, you can also make ad hoc requests:
- "Check for closed-won deals in the last 48 hours" — the agent adjusts the lookback
- "Post a handoff for the Meridian Logistics deal only" — the agent filters to a specific deal
- "What deals closed this week?" — the agent runs read-only analysis
Move a test deal to Closed Won in HubSpot, wait a few seconds, then run the skill. If no deals are found, verify the deal stage ID is closedwon — custom pipelines may use different stage IDs.
Step 5: Schedule it (optional)
Option A: Cron + Claude CLI
# Run every hour during business hours
0 8-18 * * 1-5 cd /path/to/your/project && claude -p "Run /cs-handoff" --allowedTools 'Bash(*)' 'Read(*)'Option B: GitHub Actions + Claude
name: CS Handoff Check
on:
schedule:
- cron: '0 13-23 * * 1-5' # 8 AM-6 PM ET, weekdays
workflow_dispatch: {}
jobs:
handoff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
prompt: "Run /cs-handoff"
allowed_tools: "Bash(*),Read(*)"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
CS_OWNER_ID: ${{ secrets.CS_OWNER_ID }}The processed deals file is stored locally and won't persist between GitHub Actions runs. Use GitHub Actions cache or store processed IDs in a HubSpot custom property to avoid duplicate handoffs across runs.
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).
Troubleshooting
When to use this approach
- You want conversational flexibility — ad hoc handoffs, deal-specific lookups, custom lookback windows alongside scheduled runs
- You want on-demand handoffs right after a big deal closes, not just automated notifications
- You're already using Claude Code and want skills that integrate with your workflow
- You want to run tasks in the background via Claude Cowork while focusing on other work
- You prefer guided references over rigid scripts — the agent adapts while staying reliable
When to switch approaches
- You need instant handoffs (under 1 minute after deal close) → use n8n or Zapier with webhook triggers
- You want a no-code setup with a visual builder → use Zapier or Make
- You need handoffs running 24/7 at zero LLM cost → use the Code + Cron approach
Common questions
Why not just use a script?
A script runs the same way every time. The Claude Code skill adapts to what you ask — different lookback windows, specific deals, analysis instead of action. The reference files ensure it calls the right APIs even when improvising, so you get flexibility without sacrificing reliability.
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 customize what information goes in the handoff message?
Yes. Edit templates/slack-handoff.md to add or remove fields. Or tell the agent conversationally: "Include the deal's custom field 'implementation_type' in the handoff message."
Can I run this skill on a schedule without a server?
Yes. GitHub Actions (Option B in Step 5) runs Claude on a cron schedule using GitHub's infrastructure. The free tier includes 2,000 minutes/month.
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.