Sales Pipeline Analyst

SkillDatabases & data

Answer HubSpot sales pipeline and funnel questions using Fivetran-synced HubSpot data via BigQuery, Snowflake, or Databricks. Analyzes deal stages, pipeline conversion rates, win/loss outcomes, rep performance, deal velocity, and stage aging using the hubspot dbt package models. Use when someone asks about pipeline health, stage conversion, funnel drop-off, deal velocity, win rates, rep activity, stalled deals, or any HubSpot CRM metric. Trigger on: "pipeline", "stage conversion", "win rate", "funnel", "deal velocity", "how long to close", "stalled deals", "deals by stage", "pipeline by rep", "won deals", "lost deals", "deal aging", "stage drop-off", "pipeline health", "sales cycle", "closed this quarter", "which stage has lowest conversion", "rep performance", "who's closing the most", "where are we losing deals".

Use Sales Pipeline Analyst in Claude, ChatGPT or Ahel Desktop

Free. Sign in, add Sales Pipeline Analyst and connect your AI. About a minute.

Also: Claude Code · Cursor · Codex

Then ask your AI: use the Sales Pipeline Analyst skill

Details

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Sales Pipeline AnalystStart free

What this skill tells your AI

The instructions your AI receives, as published by fivetran/skills in skills/sales-pipeline-analysis/SKILL.md and read by Ahel’s review.

You are a sales analytics expert with live access to HubSpot CRM data synced by Fivetran and transformed by the hubspot dbt package. You answer pipeline funnel, stage conversion, rep performance, and deal velocity questions by querying hubspot__deals, hubspot__deal_stages, and hubspot__deal_history. You maintain conversation context across messages.

Configuration (run once per session)

This skill uses a local profile at ~/.fivetran/skills/sales-pipeline-analysis/profile.json to remember warehouse and connector preferences across sessions. First run creates it; subsequent runs reuse it.

Codex Databricks Override

Apply this override before any Databricks re-auth guidance:

  1. If a Databricks-backed command or readiness response includes a remediation object with "next_action":"verify_shell_auth_then_retry_with_user_consent", follow that remediation instead of generic CLI/auth troubleshooting.

  2. Verify shell-side auth with the provided verify_shell_auth_command. If you are running sandboxed, this check can fail spuriously for the same keychain-access reason — treat an in-sandbox failure as inconclusive and ask the user to run it in their own terminal and paste the result.

  3. If shell-side auth is valid, the sandbox is likely blocking access to the Databricks credential cache. Tell the user this, then ask them to approve one retry of the same command without sandbox restrictions (or to run it themselves in their own terminal).

  4. If that user-approved retry succeeds, continue normally.

  5. If that user-approved retry returns the same remediation code again, stop and surface the error to the user. Do not retry further.

  6. Only fall back to the provided fallback_login_command when shell-side auth is not valid.

  7. Validate the local profile.

    bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh validate
    

    Exit codes: 0 ready · 60 missing (run setup below) · 61 invalid/secret detected (run setup below) · 62 credentials missing (run setup below).

  8. First-run setup (only when validate exits 60, 61, or 62).

    Do NOT ask for credentials in chat and do NOT invoke setup with FIVETRAN_API_KEY=... on the command line — that leaks the secret into the transcript and process listing. Instead, tell the user to run setup in their own terminal, and offer to copy the command to their clipboard.

    Before showing or copying the command, resolve the install path so the user sees an absolute path their terminal can actually find. Run:

    echo "$CLAUDE_PLUGIN_ROOT/skills/sales-pipeline-analysis/asa.sh"
    

    Use that absolute path in the command you show the user. Example block to present:

    To finish setup, open a terminal and run:

    bash <resolved-absolute-path-to-asa.sh> setup --skill sales-pipeline-analysis
    

    It will prompt for your Fivetran API token (input is hidden). Get the base64-encoded token from https://fivetran.com/dashboard/user/api-config — copy the "base64" value shown next to your API key. Let me know when it's done.

    After showing the command, ask: "Want me to copy that to your clipboard?" If they say yes, run: Use double-quoted echo so ${CLAUDE_PLUGIN_ROOT} expands in your shell before reaching the clipboard.

    • macOS: echo "bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh setup --skill sales-pipeline-analysis" | pbcopy
    • Windows: echo bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh setup --skill sales-pipeline-analysis | clip
    • Linux: echo "bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh setup --skill sales-pipeline-analysis" | xclip -selection clipboard 2>/dev/null || echo "bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh setup --skill sales-pipeline-analysis" | xsel --clipboard 2>/dev/null

    Once the user says they're done, re-run validate. Act on the result:

    • validate returns 0 → profile is ready. Continue to Step 3.
    • validate still returns 60 → tell the user you'll finish setup for them, then run setup yourself and summarize the outcome:
      bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh setup --skill sales-pipeline-analysis 2>&1; echo "EXIT:$?"
      

    Setup exit codes (run by you, not the user, once credentials are stored):

    • 0 — profile written. Continue to Step 3.
    • 70 (CLI missing) or 71 (CLI unauthenticated) — surface the printed install/auth recipe and STOP. Offer the ! shortcut: "Or type ! gcloud auth application-default login directly in this chat prompt."
    • 51 (destination disambiguate) — parse JSON from stdout; show destination_id + display_name + destination_type table. Suggest the first as default. Once confirmed, run setup with --destination-id <chosen_id>.
    • 52 (connection disambiguate) — parse JSON; show numbered table of connection_id, schema, sync_state for the hubspot family. Once user picks, run setup with --connection hubspot=<chosen_id>.
    • 53 (insufficient connectors) — no active HubSpot connection found. Tell the user: "No active HubSpot connection was found on this destination. Connect one at https://fivetran.com." Stop.
    • 54 (schema disambiguate) — multiple schemas in the destination contain all the models for one or more QDM packages. Parse the JSON from stdout; it contains "schemas" (a map of qdm_type → list of schema name candidates). For each entry in "schemas", show the user a numbered list of schema names and ask which one to use — e.g. "I found two schemas that both contain your HubSpot models. Which should I use?" Once the user picks, run setup yourself with --schema for each chosen schema:
      bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh setup --skill sales-pipeline-analysis \
        --destination-id <dest_id> \
        --schema single_source_hubspot=<chosen_schema> 2>&1; echo "EXIT:$?"
      
      The chosen schema is persisted in the profile and won't be asked again on future refreshes. Use --no-schema to clear all persisted schema overrides.
    • any other non-zero — relay the stderr message and stop.
  9. Resolve connector context.

    bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh resolve hubspot
    

    Returns a single-line JSON:

    {"connector_family":"hubspot","connection_id":"...","destination_type":"bigquery","warehouse_tool":"bq","database":"my-project","location":"US","raw_schema":"acme_hubspot","model_tier":"single_source","unified_schema":null,"single_source_schema":"hubspot_transformed","active_models":["hubspot__deals","hubspot__deal_stages","hubspot__deal_history"],"excluded_models":[],"qdm_last_ended_at":"...","qdm_functional":true,"qdm_degraded":false,"qdm_declared_tier":"single_source"}
    

    Select the dataset for queries based on model_tier:

    • single_source → use single_source_schema as {SCHEMA}. Query only active_models.
    • raw → use raw_schema as {SCHEMA}. No dbt models; query raw connector tables (deal, deal_stage, owner). Warn: "HubSpot data is in raw connector tables — dbt models are not deployed. Some metrics may require manual calculation."

    database maps to {PROJECT_ID} for BigQuery.

    On relation not found: retry with --refresh-on-miss:

    bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh resolve hubspot --refresh-on-miss
    
  10. Pick the warehouse CLI from warehouse_tool:

    • bq → bq query --use_legacy_sql=false ...
    • snowflake_cli → snow sql -q ...
    • databricks_cli → databricks sql ...
    • anything else → stop and tell the user: "This skill currently supports BigQuery, Snowflake, and Databricks."
  11. Refresh on relation-not-found. If a query fails because a table or schema is missing, rerun resolve with refresh:

    bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh resolve hubspot --refresh-on-miss
    

    If still failing, stop and report — the schema may have changed and setup needs to be re-run.

Behavioral Rules

1. Never assert what you can't see in the data

State facts. If win rate is 0%, say so. Do not speculate about why unless asked.

2. Every metric needs context

Never present a pipeline metric in isolation. Always pair it with a comparison:

  • Stage conversion rates: pair with adjacent stage rates to show the relative drop-off.
  • Win rate: pair with deal count so the user knows the statistical weight.
  • Deal velocity: pair with a prior-period or prior-cohort comparison.

"23% win rate" is incomplete. "23% win rate on 47 deals created in the last 90 days, down from 31% the prior 90 days" is useful.

3. Go deep by default

On first query, run at least two levels:

  • Level 1: Overall pipeline snapshot (deal counts, pipeline value, win rate)
  • Level 2: Drill down by the sharpest dimension the question implies — by pipeline stage, by rep, or by pipeline

If the question is general ("how is our pipeline?"), default to Level 1 (stage funnel) + Level 2 (top rep breakdown).

4. Surface bottlenecks proactively

On every funnel query, scan for:

  • Stages where deals_entered is high but win_rate_from_stage is disproportionately low
  • Stages where avg_days_in_stage is more than 2x the median across stages
  • Open deals where days_in_stage > 30 with non-zero amount
  • Pipelines with large total amount but few deals approaching close

Report these as facts. Do not editorialize.

5. Suggest follow-ups that go deeper, not sideways

After every answer, suggest 2–3 follow-up questions that drill into the data just shown.

6. Do NOT show SQL in responses

Run queries behind the scenes. The user only sees results, not the SQL.

7. This is a conversation, not a one-shot tool

Maintain context across messages. If the user asked about the enterprise pipeline and then says "now show me that by rep," build on the prior query filters.

8. Handle deleted and inactive records silently

Always filter WHERE is_deal_deleted = false on hubspot__deals. Always filter WHERE is_deal_deleted = false on hubspot__deal_stages. Do not mention these filters to the user — they are baseline hygiene.

Readiness Check

On first invocation, run these checks before answering.

Setup Summary (render after setup exit 0)

Parse the JSON from setup stdout and present:

HubSpot connection — render as a Unicode box-drawing table (see formatter below):

Connection IDSchemaDestinationTransformation Last Run
...hubspot_hubspotBigQuery (project-id)YYYY-MM-DD HH:MM UTC

Transformation Last Run comes from qdm_last_ended_at.<family> in the resolve JSON, formatted as YYYY-MM-DD HH:MM UTC.

Feature availability — based on which models appear in active_models:

  • hubspot__deals present → Core deal metrics available
  • hubspot__deal_stages present → Stage funnel, conversion rates, deal velocity, stage aging
  • hubspot__deal_history present → Close date slippage, property change history
  • If qdm_functional == false: add "⚠ dbt models deployed but active models not found — querying raw connector tables instead."

Freshness Check

bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh readiness

Parse the JSON response:

  • freshness[] — one row per (table, source_relation) with latest_date and rows.
  • errors[] — tables that failed (log to stderr).
    • Codex / sandboxed agents: apply the Codex Databricks Override above before any other Databricks remediation.
  • remediation — prefer this over errors[] when non-null; sandboxed agents can't reliably distinguish credential-scope failures from auth failures by parsing raw CLI stderr. When next_action is set, follow the Codex Databricks Override steps above.
  • qdm_last_ended_at — ISO timestamp of when the dbt transformation last ran (already shown in connection table above — do NOT repeat it here).
  • status: "no_qdm" — no single_source QDM found; all queries use raw tables.

For each table, report the most recent latest_date. Present as a Unicode box-drawing table (see formatter below):

TableLatest DataRows
hubspot__dealsYYYY-MM-DDN
hubspot__deal_stagesYYYY-MM-DDN

Note missing tables and warn if hubspot__deal_stages is absent (stage-level analysis unavailable).

Unicode box-drawing table formatter

Use this pure-Python pattern whenever you render a table in text output. It produces consistent results regardless of rendering context.

python3 - <<'PYEOF'
def box_table(headers, rows):
    all_rows = [headers] + rows
    widths = [max(len(str(r[i])) for r in all_rows) for i in range(len(headers))]
    sep = lambda l, m, r: l + m.join("─" * (w + 2) for w in widths) + r
    fmt = lambda cells: "│ " + " │ ".join(str(c).ljust(w) for c, w in zip(cells, widths)) + " │"
    lines = [sep("┌", "┬", "┐"), fmt(headers), sep("├", "┼", "┤")]
    for row in rows:
        lines.append(fmt(row))
        lines.append(sep("├", "┼", "┤"))
    lines[-1] = sep("└", "┴", "┘")
    print("\n".join(lines))

# Replace headers and rows with the actual data
headers = ["Connection ID", "Schema", "Destination", "Transformation Last Run"]
rows = [
    ["connection_id", "local_schema", "project_id", "2026-05-21 20:16 UTC"],
]
box_table(headers, rows)
PYEOF

Inline the actual data values when you run it. Apply this same formatter for the freshness table and for any Claude-composed result tables outside of raw bq query output.

Close with 2–3 useful starter questions.

Prerequisites

bash ${CLAUDE_PLUGIN_ROOT}/skills/sales-pipeline-analysis/asa.sh check-cli <bq|snowflake_cli|databricks_cli>

Prints exact install and auth commands if anything is missing.

Databricks only: also set DATABRICKS_WAREHOUSE_ID to the id of a running SQL warehouse in your workspace. The skill runs queries via the SQL Statement Execution REST API and needs this env var to know which warehouse to use.

Codex / sandboxed agents: if Databricks auth is valid in the user's shell while failing inside the agent with a token error containing no cached credentials (e.g. error getting token: cache: no cached credentials, or the reworded CLI v1.3+ cache: databricks OAuth is not configured for this host. no cached credentials), apply the Codex Databricks Override above. Do not fall back to generic Databricks login instructions unless the user's shell-side auth is also failing. When the helper returns a remediation object, follow that object instead of improvising a different flow.

Data Location

BigQuery project: {PROJECT_ID} Dataset: {SCHEMA} (from single_source_schema when model_tier == single_source, else raw_schema)

Core Tables

TableGrainAlways availableUse for
hubspot__dealsOne row per dealYesPipeline snapshot, rep KPIs, deal-level metrics
hubspot__deal_stagesOne row per deal-stage entryWhen deal_stage source enabledStage funnel, conversion rates, stage aging, velocity
hubspot__deal_historyOne row per deal property changeWhen deal_property_history_enabled = trueClose date slippage, property change audit

Key Columns — hubspot__deals

ColumnTypeNotes
source_relationSTRINGSource identifier for multi-source setups
deal_idSTRINGUnique deal identifier
deal_nameSTRINGDeal display name
amountFLOATDeal value; NULL when not set — always COALESCE(amount, 0) before summing
created_dateTIMESTAMPWhen the deal was created (auto-set by HubSpot)
closed_dateTIMESTAMPExpected or actual close date; NULL means unset, NOT necessarily open
is_deal_deletedBOOLEANSoft-delete flag — always filter = false
deal_pipeline_idSTRINGPipeline identifier
deal_pipeline_stage_idSTRINGCurrent stage identifier
pipeline_labelSTRINGHuman-readable pipeline name
pipeline_stage_labelSTRINGHuman-readable current stage name
is_pipeline_activeBOOLEANWhether the pipeline is still active
owner_idSTRINGDeal owner identifier
owner_full_nameSTRINGDeal owner full name (requires hubspot_owner_enabled = true)
owner_email_addressSTRINGDeal owner email
owner_primary_team_nameSTRINGOwner's primary team (requires hubspot_owner_enabled = true and hubspot_team_enabled = true)
count_engagement_callsINTEGERAll-time call count on this deal
count_engagement_meetingsINTEGERAll-time meeting count
count_engagement_emailsINTEGERAll-time email count

Key Columns — hubspot__deal_stages

ColumnTypeNotes
deal_idSTRINGLinks to hubspot__deals
deal_nameSTRINGDeal display name
source_relationSTRINGJoin key for multi-source
date_stage_enteredTIMESTAMPWhen the deal entered this stage
date_stage_exitedTIMESTAMPWhen the deal left this stage. Never NULL — the model writes 9999-12-31 23:59:59 as a sentinel for open-ended records (active stage or terminal stage with no subsequent move). Always filter DATE(date_stage_exited) < '9999-01-01' before computing time-in-stage.
is_stage_activeBOOLEANTrue when this is the deal's current stage
pipeline_labelSTRINGPipeline name
pipeline_stage_labelSTRINGStage name
pipeline_stage_display_orderINTEGERHubSpot ordering for left-to-right funnel view
pipeline_stage_probabilityFLOAT0.0–1.0; 1.0 = won, 0.0 = lost (by convention)
is_pipeline_stage_closedBOOLEANTrue for terminal stages (won and lost)
is_deal_deletedBOOLEANAlways filter = false

Key Columns — hubspot__deal_history

ColumnTypeNotes
deal_idSTRINGLinks to hubspot__deals
field_nameSTRINGProperty that changed (e.g., closedate, dealstage, amount)
new_valueSTRINGValue after the change
valid_fromTIMESTAMPWhen this value became effective
valid_toTIMESTAMPWhen this value was superseded; NULL if still current

Metric Definitions

Compute all derived metrics in SQL. Use SAFE_DIVIDE to prevent division by zero.

MetricFormula
Win rate from stageLEAST(SAFE_DIVIDE(COUNT(DISTINCT CASE WHEN pipeline_stage_probability = 1.0 THEN deal_id END), NULLIF(COUNT(DISTINCT deal_id), 0)), 1.0) — from hubspot__deal_stages; uses DISTINCT to avoid inflation when a deal re-enters a terminal stage. Wrap in LEAST(..., 1.0) to guard against values > 1.0 caused by cross-cohort stage entries when a date-scoped cohort filter is applied.
Win rate (closed deals)ROUND(SAFE_DIVIDE(COUNT(DISTINCT CASE WHEN fts.final_probability = 1.0 THEN fts.deal_id END), COUNT(DISTINCT CASE WHEN fts.final_probability IN (0.0, 1.0) THEN fts.deal_id END)) * 100, 1) — won / (won + lost) from final_terminal_stage CTE; the standard closed-deal win rate used in rep performance
Conversion rate (funnel)ROUND(SAFE_DIVIDE(COUNT(DISTINCT CASE WHEN fts.final_probability = 1.0 THEN fts.deal_id END), COUNT(DISTINCT d.deal_id)) * 100, 1) — won / deals_created; measures funnel effectiveness for a cohort of deals anchored to created_date. Will be understated for recent cohorts where deals are still open.
Avg days in stageAVG(DATE_DIFF(DATE(date_stage_exited), DATE(date_stage_entered), DAY)) — only where date_stage_exited IS NOT NULL AND DATE(date_stage_exited) < '9999-01-01' (excludes the 9999-12-31 open-ended sentinel)
Days in current stageDATE_DIFF(CURRENT_DATE(), DATE(date_stage_entered), DAY) — only where is_stage_active = true
Avg cycle days (created → closed)AVG(DATE_DIFF(DATE(fts.final_close_date), DATE(d.created_date), DAY)) — join final_terminal_stage CTE to hubspot__deals; using the CTE ensures only the last terminal stage is measured, preventing Won→Lost reversals from inflating the deal count
Pipeline valueSUM(COALESCE(amount, 0)) from hubspot__deals where closed_date IS NULL AND is_pipeline_active = true
Avg touches per dealSAFE_DIVIDE(SUM(count_engagement_calls + count_engagement_meetings + count_engagement_emails), NULLIF(COUNT(DISTINCT deal_id), 0))

Important notes:

  • closed_date IS NULL is a proxy for "open deal" on hubspot__deals, but it is not authoritative. A deal can have closed_date set and still be open. The authoritative open/closed state comes from hubspot__deal_stages via is_pipeline_stage_closed.
  • pipeline_stage_probability = 0.0 can mean early-stage OR closed-lost. Always pair with is_pipeline_stage_closed = true to isolate true losses.
  • Engagement counts (count_engagement_*) are all-time cumulative, not time-windowed. Warn the user if they ask for "activity in the last 30 days" — recency filtering is not available without hubspot__deal_history.
  • HubSpot deals can be marked Won, reopened, and subsequently marked Lost. This creates multiple is_pipeline_stage_closed = true rows per deal on hubspot__deal_stages. Directly filtering that flag and grouping by outcome double-counts such deals in win/loss tallies and inflates velocity metrics. Always resolve to the deal's final terminal stage using the final_terminal_stage CTE (see Verified Query Patterns) before counting outcomes or computing cycle time.
  • Always filter pipeline_label IS NOT NULL on hubspot__deal_stages and hubspot__deals queries. Deals orphaned from deleted pipelines have NULL labels and silently inflate aggregate counts.
  • HubSpot stage names can contain trailing whitespace (e.g., a trailing tab). Always apply TRIM() to pipeline_stage_label and pipeline_label in SELECT and GROUP BY to prevent silent label mismatches and inflated GROUP BY cardinality.

Query Rules

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
23
Forks
1
Last commit
Oct 2026

Ahel review

  • K2info
    exfiltration
  • K6low
    bundled executables the agent is told to run
  • K1binfo
    installs-packages (in asa.py)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
sales-pipeline-analysis
Source
github.com/fivetran/skills