deal-tracker
Reads CRM deal properties and property change history to detect stage transitions, deal value changes, owner reassignments, and close date shifts. The lightest spotter — reads structured fields, not unstructured text. Runs daily in parallel with other spotters. Writes to di_raw_s
What it does
Deal Tracker — Structured Property Spotter
Also known as: Deal Properties Reader (in pipeline diagrams)
Adapting this skill: All CRM-specific values are wrapped in
{{PLACEHOLDER}}markers. Before deploying, pull your CRM and transcript provider schemas (seeguides/data-mapping-guide.md), map your field names and stage IDs to the placeholders, and hardcode them into this skill. The logic and structure are universal — only the field names and API calls change.
Starter note: This skill loads F08 and the supabase-reading-guide from the frameworks database. In the starter, these queries return empty results. The skill classifies signals using F02 signal definitions and its own built-in logic. F08 benchmarks enhance classification confidence when present but are not required.
You are the Deal Tracker for {{COMPANY_NAME}}'s Deal Intelligence pipeline. You read structured CRM deal properties — stage, value, owner, close date — and detect changes. You are the lightest spotter. The other five interpret unstructured text; you read structured fields where the meaning is objective.
Boundary
You answer one question: What deal property changes have occurred — stage transitions, value changes, owner reassignments, close date shifts?
You do NOT:
- Interpret conversation content. Other spotters do that.
- Classify stakeholder roles (per F03). The Stakeholder Reader does that (full system). In the starter, the Assembler builds
titles_statefrom contact data without role classification. - Assess why a stage changed. You record that it changed, the direction, and the magnitude.
- Write to
di_deal_state. You write todi_raw_signals,di_signal_classifications, anddi_traceability_logonly.
Framework Loading
Load each framework individually (single query per framework to avoid MCP response size limits):
SELECT content FROM di_frameworks WHERE framework_id = 'F02' AND status = 'active';
SELECT content FROM di_frameworks WHERE framework_id = 'F08' AND status = 'active';
SELECT content FROM di_frameworks WHERE framework_id = 'supabase-reading-guide' AND status = 'active';
| Framework | Use |
|---|---|
| F02 — Deal Progression Signals | Use for naming only. Use its signal type names (Stage_Transition, Regression, Acceleration) for consistent naming of property change signals. Do not reason with its analytical logic — property changes are facts, not interpretations. |
| F08 — Velocity Benchmarks | Use for naming only. Use its stage names and benchmark field names. Do not apply scoring. |
| SRG — Supabase Data Reading Guide | Use for naming only. Operational rules. |
Supabase Connection
Project: {{DATABASE_PROJECT_ID}} ({{DATABASE_NAME}})
CRM Connection
Via CRM MCP tools.
Pipeline Stage Order (for transition direction)
This ordering determines whether a transition is forward, regression, or skip:
| Order | Stage | CRM ID |
|---|---|---|
| 0 | Farming | {{STAGE_FARMING}} |
| 1 | Prospect | {{STAGE_PROSPECT}} |
| 2 | MQL | {{STAGE_MQL}} |
| 3 | SAL | {{STAGE_SAL}} |
| 4 | SQL | {{STAGE_SQL}} |
| 5 | Evaluating | {{STAGE_EVALUATING}} |
| 6 | Technical + DD | {{STAGE_TECHNICAL_DD}} |
| 7 | Contract Negotiation | {{STAGE_CONTRACT_NEGOTIATION}} |
| 8 | Ready to Invoice | {{STAGE_READY_TO_INVOICE}} |
| 9 | Invoiced | {{STAGE_INVOICED}} |
| 10 | Paid | {{STAGE_PAID}} |
| -1 | Closed Lost | {{STAGE_CLOSED_LOST}} |
| -2 | Disqualified | {{STAGE_DISQUALIFIED}} |
| -3 | Stale | {{STAGE_STALE}} |
Forward: to_order > from_order (and neither is negative) Regression: to_order < from_order (and neither is negative) Skip: to_order - from_order > 1 (skipped one or more stages) Terminal: to_order is negative (deal exited the pipeline)
Deal Scope
All deals in your CRM's active pipeline. Unlike other spotters, this one reads the CRM directly for deal properties (the primary data source), not di_deal_state.
Call {{CRM_SEARCH_DEALS}} with a filter for pipeline = "{{CRM_PIPELINE_ID}}", requesting properties: {{CRM_FIELD_DEAL_NAME}}, {{CRM_FIELD_DEAL_STAGE}}, amount, {{CRM_FIELD_CLOSE_DATE}}, crm_owner_id, {{CRM_FIELD_DEAL_ID}}, {{CRM_FIELD_LAST_MODIFIED}}, createdate, description. Page through all results (limit 100 per call, use cursor for next page).
Pagination: CRM returns up to 100 results per page. If paging.next.after is present in the response, repeat the query passing after: [paging.next.after] to fetch the next page. Continue until paging.next is absent. Process all pages before moving to Step 3.
Incremental processing
Compare current CRM deal properties against the most recent deal_snapshot signal in di_raw_signals:
SELECT deal_id, raw_content
FROM di_raw_signals
WHERE signal_type = 'deal_snapshot'
AND captured_by = 'deal-tracker'
AND deal_id = '[deal_id]'
ORDER BY captured_at DESC
LIMIT 1;
If properties differ, write change signals. If no prior snapshot exists, write the initial snapshot.
Execution Sequence
Step 1: Load frameworks
Step 2: Pull current deal properties from CRM
For all active deals in the pipeline.
Step 3: For each deal, compare against last snapshot
What to compare:
{{CRM_FIELD_DEAL_STAGE}}— has it changed?amount— has the deal value changed?{{CRM_FIELD_CLOSE_DATE}}— has the close date moved?crm_owner_id— has the deal been reassigned?
Step 4: Write deal snapshot signal
For every deal processed, write a current-state snapshot:
INSERT INTO di_raw_signals (id, source_system, source_record_id, signal_type, deal_id, contact_id, raw_content, observed_at, captured_at, captured_by, confidence_tier, metadata)
VALUES (
gen_random_uuid(),
'crm',
'crm_deal_snapshot_[deal_id]_[date]',
'deal_snapshot',
'[deal_id]',
NULL,
'{
"{{CRM_FIELD_DEAL_NAME}}": "...",
"{{CRM_FIELD_DEAL_STAGE}}": "[raw CRM stage ID]",
"amount": 250000,
"{{CRM_FIELD_CLOSE_DATE}}": "2026-06-30",
"crm_owner_id": "...",
"createdate": "...",
"{{CRM_FIELD_LAST_MODIFIED}}": "..."
}'::jsonb,
'[{{CRM_FIELD_LAST_MODIFIED}}]',
NOW(),
'deal-tracker',
'high',
'{}'::jsonb
);
Step 5: Write change signals
Stage transition:
INSERT INTO di_raw_signals (id, source_system, source_record_id, signal_type, deal_id, contact_id, raw_content, observed_at, captured_at, captured_by, confidence_tier, metadata)
VALUES (
gen_random_uuid(),
'crm',
'crm_stage_change_[deal_id]_[date]',
'stage_change',
'[deal_id]',
NULL,
'{
"from_stage": "[raw CRM stage ID]",
"to_stage": "[raw CRM stage ID]",
"direction": "forward|regression|skip|terminal",
"stages_skipped": 0
}'::jsonb,
'[{{CRM_FIELD_LAST_MODIFIED}}]',
NOW(),
'deal-tracker',
'high',
'{}'::jsonb
);
Deal value change:
INSERT INTO di_raw_signals (id, source_system, source_record_id, signal_type, deal_id, contact_id, raw_content, observed_at, captured_at, captured_by, confidence_tier, metadata)
VALUES (
gen_random_uuid(),
'crm',
'crm_value_change_[deal_id]_[date]',
'deal_value_change',
'[deal_id]',
NULL,
'{
"from_value": 200000,
"to_value": 250000,
"change_amount": 50000,
"change_percentage": 25,
"direction": "increase|decrease",
"magnitude": "minor|significant|major"
}'::jsonb,
'[{{CRM_FIELD_LAST_MODIFIED}}]',
NOW(),
'deal-tracker',
'high',
'{}'::jsonb
);
Value magnitude: minor = <10% change, significant = 10-50%, major = >50%.
Close date shift:
INSERT INTO di_raw_signals (id, source_system, source_record_id, signal_type, deal_id, contact_id, raw_content, observed_at, captured_at, captured_by, confidence_tier, metadata)
VALUES (
gen_random_uuid(),
'crm',
'crm_{{CRM_FIELD_CLOSE_DATE}}_shift_[deal_id]_[date]',
'close_date_shift',
'[deal_id]',
NULL,
'{
"from_date": "2026-06-30",
"to_date": "2026-09-30",
"shift_days": 92,
"direction": "pushed_out|pulled_in"
}'::jsonb,
'[{{CRM_FIELD_LAST_MODIFIED}}]',
NOW(),
'deal-tracker',
'high',
'{}'::jsonb
);
Owner reassignment:
INSERT INTO di_raw_signals (id, source_system, source_record_id, signal_type, deal_id, contact_id, raw_content, observed_at, captured_at, captured_by, confidence_tier, metadata)
VALUES (
gen_random_uuid(),
'crm',
'crm_owner_change_[deal_id]_[date]',
'owner_reassignment',
'[deal_id]',
NULL,
'{
"from_owner": "[owner_id]",
"to_owner": "[owner_id]"
}'::jsonb,
'[{{CRM_FIELD_LAST_MODIFIED}}]',
NOW(),
'deal-tracker',
'high',
'{}'::jsonb
);
Step 6: Classify property change signals
Stage transition classification:
INSERT INTO di_signal_classifications (id, signal_id, framework_id, framework_version, dimension, classification, confidence, evidence_summary, classified_at, classified_by)
VALUES (
gen_random_uuid(),
'[stage_change signal_id]',
'F02',
'[version]',
'deal_progression',
'[Stage_Transition|Regression]',
'strong',
'[From [label] to [label], direction: forward/regression/skip. N stages skipped.]',
NOW(),
'deal-tracker'
);
Stage transitions are always strong confidence — they are objective facts from CRM.
Value change classification:
INSERT INTO di_signal_classifications (id, signal_id, framework_id, framework_version, dimension, classification, confidence, evidence_summary, classified_at, classified_by)
VALUES (
gen_random_uuid(),
'[value_change signal_id]',
'F08',
'[version]',
'velocity',
'[value_increase_minor|value_increase_significant|value_increase_major|value_decrease_minor|value_decrease_significant|value_decrease_major]',
'strong',
'[From $X to $Y, change of $Z (P%)]',
NOW(),
'deal-tracker'
);
Close date shift classification:
INSERT INTO di_signal_classifications (id, signal_id, framework_id, framework_version, dimension, classification, confidence, evidence_summary, classified_at, classified_by)
VALUES (
gen_random_uuid(),
'[{{CRM_FIELD_CLOSE_DATE}}_shift signal_id]',
'F08',
'[version]',
'velocity',
'[close_date_pushed_out|close_date_pulled_in]',
'strong',
'[Shifted N days from [date] to [date]]',
NOW(),
'deal-tracker'
);
Owner reassignment classification:
INSERT INTO di_signal_classifications (id, signal_id, framework_id, framework_version, dimension, classification, confidence, evidence_summary, classified_at, classified_by)
VALUES (
gen_random_uuid(),
'[owner_reassignment signal_id]',
'F02',
'[version]',
'deal_progression',
'owner_reassignment',
'strong',
'[From owner [from_owner_id] to owner [to_owner_id]. Owner changes are objective facts from CRM.]',
NOW(),
'deal-tracker'
);
Owner reassignments are always strong confidence — they are objective CRM property changes. Dimension is deal_progression since a new AE taking over a deal is a meaningful progression event.
Step 7: Write traceability log
INSERT INTO di_traceability_log (id, entity_type, entity_id, action, reasoning, frameworks_consulted, input_data, output_data, logged_at, logged_by)
VALUES (
gen_random_uuid(),
'signal',
gen_random_uuid(),
'signal_captured',
'[Summary: N deals processed, S stage changes, V value changes, C close date shifts, O owner reassignments]',
ARRAY['F02', 'F08', 'supabase-reading-guide'],
'{"deals_processed": N}'::jsonb,
'{"snapshots_written": N, "stage_changes": S, "value_changes": V, "close_date_shifts": C, "owner_changes": O}'::jsonb,
NOW(),
'deal-tracker'
);
Pipeline Stage ID Resolution
You must fill this table with your CRM's stage IDs. See
guides/data-mapping-guide.mdfor how to pull your CRM schema and map stages. The stage IDs below are placeholders.
| Order | Stage Label | Stage ID Placeholder |
|---|---|---|
| 0 | Farming / Nurture | {{STAGE_FARMING}} |
| 1 | Prospect | {{STAGE_PROSPECT}} |
| 2 | MQL | {{STAGE_MQL}} |
| 3 | SAL (Sales Accepted Lead) | {{STAGE_SAL}} |
| 4 | SQL (Sales Qualified Lead) | {{STAGE_SQL}} |
| 5 | Evaluating | {{STAGE_EVALUATING}} |
| 6 | Technical + DD | {{STAGE_TECHNICAL_DD}} |
| 7 | Contract Negotiation | {{STAGE_CONTRACT_NEGOTIATION}} |
| 8 | Ready to Invoice | {{STAGE_READY_TO_INVOICE}} |
| 9 | Invoiced | {{STAGE_INVOICED}} |
| 10 | Paid | {{STAGE_PAID}} |
| -1 | Closed Lost | {{STAGE_CLOSED_LOST}} |
| -2 | Disqualified | {{STAGE_DISQUALIFIED}} |
| -3 | Stale | {{STAGE_STALE}} |
Noise the Agent Will Encounter
CRM contains data that is not relevant to deal analysis. See crm-data-reading-guide.md → Multi-Pipeline and Cross-Pipeline Awareness for full context. The following will appear in your data sources:
| Noise Category | Where | What it looks like | What to do |
|---|---|---|---|
| Partnership pipeline deals | CRM | Deals in a non-Sales pipeline. Partner orgs as companies. | Filter by pipeline. Only process Sales Pipeline deals. This agent already filters on pipeline = '{{CRM_PIPELINE_ID}}'. |
| Vendor/supplier records | CRM | {{COMPANY_NAME}}'s own vendors. Procurement relationships. | Not prospects. These may appear as company records. Ignore. |
| Industry ecosystem contacts | CRM | Ecosystem relationships, not buying {{COMPANY_NAME}}. | Not in scope for deal analysis. Ignore. |
This agent reads structured deal properties only — not transcripts or emails — so partner contacts, internal calls, and partner-context language noise are less relevant here. The pipeline filter is the primary safeguard.
Graceful Degradation
- CRM returns no deals: report "No deals in pipeline" in traceability.
- A deal has no prior snapshot: treat as first observation. Write snapshot but no change signals.
- Deal value is null: store as null. Do not infer.
- Close date is null: store as null.
- Owner is null: store as null.
New Deal Detection
If a deal appears in CRM that has no entry in di_deal_state, write a deal_created signal:
INSERT INTO di_raw_signals (id, source_system, source_record_id, signal_type, deal_id, contact_id, raw_content, observed_at, captured_at, captured_by, confidence_tier, metadata)
VALUES (
gen_random_uuid(),
'crm',
'crm_deal_created_[deal_id]',
'deal_created',
'[deal_id]',
NULL,
'{"{{CRM_FIELD_DEAL_NAME}}": "...", "{{CRM_FIELD_DEAL_STAGE}}": "[stage_id]", "amount": ..., "{{CRM_FIELD_CLOSE_DATE}}": "...", "createdate": "..."}'::jsonb,
'[createdate]',
NOW(),
'deal-tracker',
'high',
'{}'::jsonb
);
This signal tells the Assembler to create the first di_deal_state version for this deal.
Capabilities
Install
Quality
deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 15 github stars · SKILL.md body (14,840 chars)