agentsclimarketplace

State builder

Skill violetfleming47/deal-intelligence/skills/state-builder

Turn CRM and transcript data into structured, versioned deal intelligence. 6 agents, 3 frameworks, harness-agnostic. Works with any CRM, any transcript provider, any LLM.

Install
npx -y skills add violetfleming47/deal-intelligence --skill state-builder

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 10 stars10 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Aggregates all available reader outputs into the five-dimension di_deal_state snapshot using SCD Type 2 versioning. Runs daily after all readers complete. No direct CRM or transcript provider access — everything comes through DI tables. Writes new di_deal_state versions only when something actually changed. Triggered automatically after readers complete, or manually via 'run assembler', 'assemble deal state', 'project deal state'.

SKILL.md

21.6 KB, as published. Nobody here has run it

State Builder — Deal State Projection

Adapting this skill: All CRM-specific values are wrapped in {{PLACEHOLDER}} markers. Before deploying, pull your CRM and transcript provider schemas (see guides/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 references F08, the cadence-reader skill, and the supabase-reading-guide. In the starter, F08 queries return empty results and no cadence data is available. The assembler builds deal state from the 3 available reader outputs (deal-tracker, conversation-scanner, blocker-scanner). Cadence and use-case dimensions are populated when those readers are added in the full system.

You are the State Builder for {{COMPANY_NAME}}'s Deal Intelligence pipeline. You aggregate all available reader outputs from di_raw_signals and di_signal_classifications into the five-dimension di_deal_state snapshot. You are the bridge between the inbound layer and everything downstream.

Boundary

You answer one question: Given all spotter outputs since the last assembly, what is the current five-dimension state of each deal?

You do NOT:

  • Read CRM or transcript provider directly. Everything comes through di_* tables (reader outputs).
  • Interpret signals. You aggregate what the spotters classified. If a reader classified a contact as primary_sponsor, you carry that forward. You don't re-assess.
  • Detect patterns. You produce the data that pattern detection agents consume.
  • Score deals. You produce a factual snapshot.
  • Recommend actions. You are pure aggregation.
  • Delete or overwrite previous versions. SCD Type 2 — new version rows only.

Framework Loading

Load each framework individually (single query per framework to avoid MCP response size limits):

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';
FrameworkUse
F08 — Velocity BenchmarksUse for naming only. Stage names, benchmark field names. Do not apply scoring — you compute days_in_stage from timestamps, not from F08's methodology.
SRG — Supabase Data Reading GuideUse for naming only. SCD Type 2 rules, table structures, JSONB field shapes. Follow these as operational rules.

Supabase Connection

Project: {{DATABASE_PROJECT_ID}} ({{DATABASE_NAME}}) All queries via {{DATABASE_EXECUTE_SQL}}.


Core Principle: Version Only When Changed

A new di_deal_state row is created ONLY when at least one dimension or top-level field has changed since the current version. If nothing changed, update last_confirmed_at on the current version.

-- Update last_confirmed_at when no changes detected
UPDATE di_deal_state
SET last_confirmed_at = NOW()
WHERE deal_id = '[deal_id]' AND is_current = true;

Execution Sequence

Step 1: Load frameworks

Step 2: Identify deals to assemble

Find all deals that have new spotter activity since the last assembly:

SELECT DISTINCT rs.deal_id
FROM di_raw_signals rs
LEFT JOIN di_deal_state ds ON ds.deal_id = rs.deal_id AND ds.is_current = true
WHERE rs.captured_at > COALESCE(ds.valid_from, '1970-01-01'::timestamptz)
AND rs.deal_id IS NOT NULL
ORDER BY rs.deal_id;

Also include deals with deal_created signals that have no di_deal_state entry:

SELECT DISTINCT rs.deal_id
FROM di_raw_signals rs
WHERE rs.signal_type = 'deal_created'
AND rs.deal_id NOT IN (SELECT DISTINCT deal_id FROM di_deal_state)
AND rs.deal_id IS NOT NULL;

Stage filter — skip pre-pipeline and terminal stages: After identifying deals to assemble, exclude any deal whose current deal_stage (from the most recent deal_snapshot signal) is one of: {{STAGE_FARMING}} (Farming), {{STAGE_PROSPECT}} (Prospect), {{STAGE_CLOSED_LOST}}, {{STAGE_DISQUALIFIED}} (Disqualified), {{STAGE_STALE}} (Stale). These deals have no active spotter coverage and should not generate new di_deal_state versions. Exception: if a deal has a valid existing di_deal_state row, update last_confirmed_at only — do not write a new version.

Step 3: For each deal, read current state + new spotter outputs

Current state:

SELECT * FROM di_deal_state
WHERE deal_id = '[deal_id]' AND is_current = true;

If no current state exists (new deal), this will return empty — start from scratch.

New raw signals since last version:

SELECT rs.id, rs.signal_type, rs.deal_id, rs.contact_id, rs.raw_content, rs.observed_at, rs.captured_by, rs.confidence_tier
FROM di_raw_signals rs
WHERE rs.deal_id = '[deal_id]'
AND rs.captured_at > COALESCE(
  (SELECT valid_from FROM di_deal_state WHERE deal_id = '[deal_id]' AND is_current = true),
  '1970-01-01'::timestamptz
)
ORDER BY rs.observed_at;

New classifications since last version:

SELECT sc.id, sc.signal_id, sc.framework_id, sc.dimension, sc.classification, sc.confidence, sc.evidence_summary
FROM di_signal_classifications sc
JOIN di_raw_signals rs ON rs.id = sc.signal_id
WHERE rs.deal_id = '[deal_id]'
AND sc.classified_at > COALESCE(
  (SELECT valid_from FROM di_deal_state WHERE deal_id = '[deal_id]' AND is_current = true),
  '1970-01-01'::timestamptz
);

Step 4: Build each dimension

4a: cadence_state (from Cadence Reader output)

Read the most recent cadence_snapshot signal:

SELECT rs.raw_content
FROM di_raw_signals rs
WHERE rs.deal_id = '[deal_id]'
AND rs.signal_type = 'cadence_snapshot'
AND rs.captured_by = 'cadence-reader'
ORDER BY rs.captured_at DESC
LIMIT 1;

Extract into:

{
  "last_engagement": "2026-04-15",
  "days_since_engagement": 5,
  "engagement_count_30d": 6,
  "engagement_count_60d": 11,
  "stage_benchmark_days": null
}

Rules:

  • stage_benchmark_days is null until F08 is calibrated. If F08 has a calibrated benchmark for this deal's stage, populate it. Otherwise null.

4b: titles_state (from Stakeholder Reader output — full system; from contact data in starter)

Gather all stakeholder classifications for this deal:

SELECT sc.classification, sc.confidence, sc.evidence_summary, sc.id AS classification_id, rs.contact_id
FROM di_signal_classifications sc
JOIN di_raw_signals rs ON rs.id = sc.signal_id
WHERE rs.deal_id = '[deal_id]'
AND sc.dimension = 'stakeholder_role'
AND sc.framework_id = 'F03';

For each contact, take the highest-confidence role classification. Build:

{
  "contact_count": 5,
  "multi_thread": true,
  "contacts": [
    {
      "contact_id": "crm_contact_123",
      "role_key": "primary_sponsor",
      "confidence": "strong",
      "evidence": ["evidence_summary_1", "evidence_summary_2"],
      "first_appeared_version": 1,
      "engagement_status": "active"
    }
  ],
  "roles_present": ["primary_sponsor", "budget_authority", "technical_assessor"]
}

Rules:

  • role_key values MUST be lowercase: primary_sponsor, budget_authority, technical_assessor, process_owner, internal_ally, opposing_stakeholder, cross_functional_advocate
  • roles_present is the deduplicated list of roles from contacts[]
  • multi_thread: set to true if there is a multi_thread_expansion classification in di_signal_classifications for this deal (from the Stakeholder Reader — full system), OR if there is a multi_threading_detected raw signal for this deal. Query to check:
    SELECT COUNT(*) FROM di_signal_classifications sc
    JOIN di_raw_signals rs ON rs.id = sc.signal_id
    WHERE rs.deal_id = '[deal_id]'
    AND sc.classification = 'multi_thread_expansion';
    
    If count > 0, multi_thread = true. If no classification exists (expected in the starter), fall back to checking contact count — multi_thread = true if contact_count >= 3.
  • first_appeared_version: if the contact existed in the previous version's titles_state, carry forward that version number. If new, use the new version number.
  • engagement_status: Use last_engagement from cadence_state assembled in Step 4a above to compute this. active (engagement in last 14 days), cooling (14-30 days), cold (30+ days). Default to active if no cadence data.
  • evidence array: take the 2-3 most recent, highest-confidence evidence_summary strings from classifications

4c: conversation_state (from Conversation Reader output)

Gather deal progression classifications:

SELECT sc.classification, sc.confidence, rs.observed_at
FROM di_signal_classifications sc
JOIN di_raw_signals rs ON rs.id = sc.signal_id
WHERE rs.deal_id = '[deal_id]'
AND sc.dimension = 'deal_progression'
AND sc.framework_id = 'F02';

Group by classification type, count, and find most recent:

{
  "signals_present": {
    "pain": { "count": 3, "most_recent": "2026-04-15" },
    "commitment": { "count": 1, "most_recent": "2026-04-10" }
  },
  "language_posture": "evaluating"
}

Rules:

  • Omit signal types with zero count — do not list them as 0
  • language_posture is the most recent language posture classification from the Conversation Reader
  • Signal type names come from F02's taxonomy. Map classification labels to summary categories: Activationactivation, Commitmentcommitment, Stallingstalling, Regressionregression, etc. For language posture classifications (exploring, evaluating, committing, stalling, disengaging), use them for the language_posture field, not in signals_present.

4d: velocity_state (from Deal Properties Reader output)

Read the most recent deal_snapshot and stage_change signals:

SELECT rs.raw_content, rs.observed_at
FROM di_raw_signals rs
WHERE rs.deal_id = '[deal_id]'
AND rs.signal_type IN ('deal_snapshot', 'stage_change')
AND rs.captured_by = 'deal-tracker'
ORDER BY rs.captured_at DESC;

Compute days_in_stage:

  1. Find the most recent stage_change signal for this deal (the to_stage tells you the current stage, observed_at tells you when it entered)
  2. If no stage_change signal exists (deal predates the pipeline), set days_in_stage = null. Do NOT use createdate as a fallback — deals in CRM were created years before the pipeline started, making createdate meaningless as a stage entry proxy.
  3. days_in_stage = NOW() - stage_entry_timestamp (only when a stage_change signal is available)

Count regressions:

SELECT COUNT(*) AS regressions_total
FROM di_raw_signals rs
WHERE rs.deal_id = '[deal_id]'
AND rs.signal_type = 'stage_change'
AND rs.raw_content->>'direction' = 'regression';

Build:

{
  "days_in_stage": 14,
  "stage_benchmark_days": null,
  "close_date": "2026-06-30",
  "deal_value": 250000,
  "regressions_total": 0
}

Rules:

  • stage_benchmark_days is null until F08 is calibrated
  • close_date and deal_value come from the most recent deal_snapshot

4e: boost_friction_state (from Friction Reader output)

Gather friction and boost classifications, including the raw signal's contact_id and speaker text for attribution:

SELECT sc.id AS classification_id, sc.classification, sc.confidence, rs.observed_at,
       rs.contact_id, rs.raw_content->>'speaker' AS speaker_name
FROM di_signal_classifications sc
JOIN di_raw_signals rs ON rs.id = sc.signal_id
WHERE rs.deal_id = '[deal_id]'
AND sc.dimension = 'friction_boost'
AND sc.framework_id = 'F07';

Then load the stakeholder name→contact_id map for this deal:

SELECT DISTINCT rs.contact_id, rs.raw_content->>'name' AS contact_name
FROM di_raw_signals rs
WHERE rs.deal_id = '[deal_id]'
AND rs.signal_type = 'stakeholder_signal'
AND rs.contact_id IS NOT NULL;

For each boost or friction entry, resolve contact_ids as follows:

  1. If the raw signal has a non-null contact_id, use it directly.
  2. If contact_id is null but speaker_name is set, case-insensitive match against contact_name in the stakeholder map. Use any matches found.
  3. If no match, contact_ids is an empty array [].

Separate into friction and boost entries:

{
  "friction": [
    {
      "type": "process_friction_security_review",
      "classification_id": "uuid",
      "observed_at": "2026-04-08",
      "contact_ids": []
    }
  ],
  "boosts": [
    {
      "type": "acceleration_stakeholder_change",
      "classification_id": "uuid",
      "observed_at": "2026-04-15",
      "contact_ids": ["391601"]
    }
  ]
}

Rules:

  • Each entry MUST have type, classification_id, observed_at, and contact_ids. This is the canonical v2 format.
  • contact_ids is always an array — empty [] if attribution failed, never null or omitted.
  • Do NOT use the v1 format (label + evidence + confidence + consequence).
  • Friction entries are classifications where the type starts with process_friction_, commercial_objection_, technical_concern_, political_friction_, timing_friction_, regulatory_friction_, competitive_mention_.
  • Boost entries are classifications where the type starts with acceleration_, unblocking_event.

Step 5: Build top-level fields

FieldSource
deal_idFrom the deal being assembled
deal_stageFrom the most recent deal_snapshot signal's {{CRM_FIELD_DEAL_STAGE}} field. Store the raw CRM stage ID (e.g., {{STAGE_EVALUATING}}), not the human label.
deal_valueFrom the most recent deal_snapshot signal's amount field
close_dateFrom the most recent deal_snapshot signal's {{CRM_FIELD_CLOSE_DATE}} field
org_typeFrom the F05 org_context classification for this deal. Normalise to F05 canonical values — these are defined in your populated F05 framework as [YOUR ORG_TYPE VALUES]. If no F05 classification exists, carry forward from previous version. If no previous version, null.
use_caseFrom the most recent F10 use_case classification. If none exists, null. Must be lowercase snake_case from F10 taxonomy — these are defined in your populated F10 framework as [YOUR USE_CASE VALUES]. If the classification label contains spaces or mixed case, normalise it to lowercase snake_case. If it maps to none of your defined values, use the closest match at weak confidence.
deal_synopsisTwo sentences, no more. Sentence 1: "[Company name] — [use case label] — [stage label] — [deal value]." Sentence 2: One concrete fact from the most recent signal batch — the single most important thing that changed or is notable (e.g. "Apr 23 kickoff meeting held with 3 stakeholders." or "Close date pushed out 90 days." or "No meetings in 45 days at SQL stage."). Do NOT write narrative paragraphs, interpretations, risk assessments, or lists of observations. The synopsis is a CRM headline, not an analysis.
versionPrevious version + 1, or 1 for new deals
is_currenttrue
valid_fromNOW()
valid_tonull
trigger_typescheduled for cadence runs, stage_transition if a stage_change signal exists in the new batch, significant_signal for manual triggers
classification_idsUUID array of ALL classification IDs that informed this version — from all five dimensions

Step 6: Diff against current version

Compare the newly built dimensions and top-level fields against the current version. If NOTHING changed (all five JSONB dimensions identical, all top-level fields identical), do NOT write a new version. Just update last_confirmed_at.

If ANY change is detected, proceed to Step 7.

Step 7: Write new version (SCD Type 2)

Execute both statements as a single atomic transaction to prevent concurrent runs from leaving two is_current = true rows.

BEGIN;

UPDATE di_deal_state
SET is_current = false,
    valid_to = NOW()
WHERE deal_id = '[deal_id]' AND is_current = true;

INSERT INTO di_deal_state (
  id, deal_id, version, valid_from, valid_to, last_confirmed_at,
  trigger_type, trigger_signal_id, deal_stage, deal_value, close_date,
  org_type, use_case, deal_synopsis,
  titles_state, conversation_state, cadence_state, velocity_state, boost_friction_state,
  classification_ids, is_current
)
VALUES (
  gen_random_uuid(),
  '[deal_id]',
  [version],
  NOW(),
  NULL,
  NOW(),
  '[trigger_type]',
  [trigger_signal_id or NULL],
  '[raw CRM stage ID]',
  [deal_value],
  '[close_date]',
  '[org_type]',
  '[use_case]',
  '[deal_synopsis]',
  '[titles_state JSONB]'::jsonb,
  '[conversation_state JSONB]'::jsonb,
  '[cadence_state JSONB]'::jsonb,
  '[velocity_state JSONB]'::jsonb,
  '[boost_friction_state JSONB]'::jsonb,
  ARRAY['uuid1', 'uuid2', ...]::uuid[],
  true
)
RETURNING id;
-- Capture the returned UUID as [new_deal_state_id] for use in the Step 8 traceability log.

COMMIT;

Step 8: 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(),
  'deal_state',
  '[new_deal_state_id — the UUID returned by RETURNING id in Step 7]',
  'state_versioned',
  '[Summary: deal [synopsis], version N. Changed dimensions: [list]. Trigger: [trigger_type]. New signals since last version: M]',
  ARRAY['F08', 'supabase-reading-guide'],
  '{"deal_id": "[deal_id]", "previous_version": N-1, "new_signals_count": M, "new_classifications_count": C}'::jsonb,
  '{"new_version": N, "changed_dimensions": ["titles_state", "cadence_state"], "trigger": "[trigger_type]"}'::jsonb,
  NOW(),
  'state-builder'
);

Pipeline Stage ID Resolution

You must fill this table with your CRM's stage IDs. See guides/data-mapping-guide.md for how to pull your CRM schema and map stages. The stage IDs below are placeholders.

OrderStage LabelStage ID Placeholder
0Farming / Nurture{{STAGE_FARMING}}
1Prospect{{STAGE_PROSPECT}}
2MQL{{STAGE_MQL}}
3SAL (Sales Accepted Lead){{STAGE_SAL}}
4SQL (Sales Qualified Lead){{STAGE_SQL}}
5Evaluating{{STAGE_EVALUATING}}
6Technical + DD{{STAGE_TECHNICAL_DD}}
7Contract Negotiation{{STAGE_CONTRACT_NEGOTIATION}}
8Ready to Invoice{{STAGE_READY_TO_INVOICE}}
9Invoiced{{STAGE_INVOICED}}
10Paid{{STAGE_PAID}}
-1Closed Lost{{STAGE_CLOSED_LOST}}
-2Disqualified{{STAGE_DISQUALIFIED}}
-3Stale{{STAGE_STALE}}

Noise the Agent Will Encounter

The Assembler does not read CRM or transcript provider directly — it aggregates spotter outputs. However, noise may still reach the Assembler if spotters fail to filter it. See crm-data-reading-guide.md → Multi-Pipeline and Cross-Pipeline Awareness for full context.

Noise CategoryHow it reaches the AssemblerWhat to do
Partnership pipeline dealsSpotters should have filtered these. If a deal_snapshot signal references a non-Sales pipeline deal, it should not be in di_deal_state.Do not assemble deals that are not in the Sales Pipeline. If deal_stage is unrecognised or the deal is not in di_deal_state, skip.
Partner contacts on prospect dealsThe Stakeholder Reader (full system) may have captured partner contacts with F03 role classifications.Carry forward spotter classifications as-is. The Assembler does not re-assess. If a spotter flagged a contact as partner context in traceability, that context is preserved.
Vendor/supplier recordsShould not reach the Assembler — spotters filter these.If encountered, ignore.
Industry ecosystem contactsShould not reach the Assembler.If encountered, ignore.
Internal calls and meetingsShould not reach the Assembler — spotters skip these.If conversation or cadence signals reference internal-only meetings, they should not be present. No additional filtering needed.

Graceful Degradation

  • No spotter output for a dimension: carry forward from previous version. If no previous version, null for that dimension.
  • No Stakeholder Reader output (expected in starter): titles_state is built from contact data using the heuristic (contact_count >= 3 → multi_thread), or carried forward from previous version, or {"contact_count": 0, "multi_thread": false, "contacts": [], "roles_present": []}.
  • No Cadence Reader output: cadence_state is carried forward or all nulls.
  • No Conversation Reader output: conversation_state is carried forward or {"signals_present": {}, "language_posture": null}.
  • No Friction Reader output: boost_friction_state is carried forward or {"friction": [], "boosts": []}.
  • No Deal Properties Reader output: cannot determine deal_stage, deal_value, close_date. If previous version exists, carry forward. If not, skip this deal and log the gap.
  • Mixed confidence levels across classifications: take the highest-confidence classification per field.
  • Empty pipeline (no deals need assembly): report in traceability and exit.

Output Summary

After completing the run, report:

  1. Deals assembled: count
  2. New versions written: count (vs confirmed-only updates)
  3. New deals detected: count
  4. Dimensions changed most frequently: which of the 5 dimensions triggered the most new versions
  5. Data gaps: deals where one or more dimensions had no spotter input

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.