HarborCRM Skill — Transferable Rules
Environment
Use GDPEVO_ENV_BASE_URL as the API base URL. Never use localhost or run setup scripts directly.
API Overview
HarborCRM exposes these public endpoint families (replace {id} with the task-specific identifier):
| Category |
Endpoints |
| Events |
/api/events/{event_id}, /api/events/{event_id}/orders, /api/events/{event_id}/badges, /api/events/{event_id}/sponsor_packages |
| Finance |
/api/finance/invoices?event_id={event_id} |
| CRM |
/api/crm/accounts, /api/crm/contacts, /api/crm/opportunities |
| Campaigns |
/api/crm/campaign_members?event_id={event_id} |
| Tradeshows |
/api/tradeshows, /api/tradeshows/{show_id}/exhibitors, /api/tradeshows/{show_id}/meeting_interest |
| Import |
/api/import_batches, /api/import_batches/{batch_id}/raw_contacts, /api/import_batches/{batch_id}/suppression |
| Config |
/api/policies — returns policy metadata including opportunity amounts, follow-up windows, qualification rules |
Always start by fetching /api/policies — it encodes task-specific business rules (qualification criteria, opportunity amounts, follow-up windows).
Data Normalization
Email
- Trim whitespace, convert to lowercase.
- When no email is available, use an empty string
"".
- Deduplication keys use format:
"email:<lowercase_email>".
Phone
- Strip all non-digit characters. Keep only
[0-9].
- When no phone is available, use an empty string
"".
Sponsor Status Logic
Derive each sponsor's status from their invoice data:
| Condition |
Status |
Invoice exists AND paid_amount >= total_amount (fully paid) |
paid_deferred |
Invoice exists AND 0 < paid_amount < total_amount (partial) |
open_invoice |
| No invoice exists for the sponsor package |
proposal_only |
For open_invoice status:
open_balance = total_amount - paid_amount
- For
paid_deferred and proposal_only, open_balance = 0.
All monetary values are integers in USD.
Sponsor Revenue Totals
Sum package amounts by status:
paid_deferred: sum of all paid_deferred sponsor package amounts
open_invoice: sum of all open_invoice sponsor package amounts
proposal_only: sum of all proposal_only sponsor package amounts
open_invoice_balance: sum of all open balances from open_invoice sponsors
Qualified Lead / Account Logic
Qualification (Event/Exhibitor Tasks)
- A contact is a qualified non-sponsor lead if ALL of:
- Their company is NOT a sponsor (sponsor attendees are excluded).
- Their CRM account is NOT already disqualified.
- Their badge is a business badge (not non-business like student/press).
- For trade-show tasks: the exhibitor meets the platform/product criteria in the policy (e.g., makes/OEM-builds the target product category).
CRM Action Determination
- For each qualified lead, check
/api/crm/accounts and /api/crm/contacts:
- If a matching CRM account exists →
crm_account_action: "update_existing", set account_id.
- If no matching CRM account →
crm_account_action: "create_account", account_id: null.
- If a matching CRM contact exists for that account →
crm_contact_action: "update_existing", set contact ID.
- If no matching CRM contact →
crm_contact_action: "create_contact".
- Campaign member action for qualified leads is always
"add_campaign_member".
Opportunity Amount
- Use the event's
lead_opportunity_amount (from /api/events/{id} or /api/policies) for each qualified non-sponsor account.
lead_pipeline_total = sum of opportunity amounts across all qualified lead accounts.
Exclusion Categories
| Reason |
When to apply |
sponsor_attendee |
Contact's company is an active event sponsor |
existing_disqualified |
CRM account exists and is already disqualified |
non_business_badge |
Badge type is non-business (student, press, speaker, etc.) |
inactive_sponsor_record |
Sponsor record is canceled/inactive |
distributor_only |
Exhibitor is a distributor, not a manufacturer/OEM |
service_only |
Exhibitor provides services only |
sensor_vendor_only / sensor_only |
Exhibitor sells sensors but not the target platform |
research_only |
Exhibitor is a research institution |
not_target_market |
Exhibitor doesn't serve the target market |
missing_contact |
Row has no contact name |
Use the "relationship_type" field from the exhibitor data to determine the matching exclusion reason.
Deduplication
When processing import batches:
- Duplicate key =
"email:<normalized_lowercase_email>".
- Winner: the row with the lowest
row_id (lexicographic/numeric ascending) among duplicates.
- All other rows for that key are marked as
"duplicate" with reason and go into removed_rows.
duplicate_removed_count = total number of removed duplicate rows (not including the winner).
Suppression
From /api/import_batches/{batch_id}/suppression:
- Any raw contact whose normalized email matches a suppression entry is marked
crm_action: "suppress" (or "no_import"), reason "suppressed", and added to removed_rows.
- Suppressed contacts do NOT appear in
clean_contacts.
Follow-Up Due Dates
Compute from the event date (from /api/events/{id}):
- Lead follow-up due date: event date + 90 days.
- Sponsor finance due date: event date + 87 days.
- Format:
"YYYY-MM-DD".
Follow-Up Task Counts
lead_task_count = number of qualified_lead_accounts.
sponsor_finance_task_count = number of sponsor accounts with open_invoice or proposal_only status (unpaid sponsors).
sponsor_finance_accounts = list of account names for those unpaid sponsors, sorted alphabetically ascending.
CRM Action Counts
Count across all qualified lead accounts:
accounts_create: count where crm_account_action == "create_account".
accounts_update: count where crm_account_action == "update_existing".
contacts_create: count where crm_contact_action == "create_contact".
contacts_update: count where crm_contact_action == "update_existing".
campaign_members_create: count of "add_campaign_member" actions (= number of qualified leads).
campaign_members_update: count where existing campaign members are updated (typically 0 in fresh handoffs).
Import Batch CRM Actions
For batch import tasks, crm_action per clean contact:
"create_account" — company not in CRM, valid contact data.
"update_existing" — company found in CRM (match by account name).
"no_import" — removed (missing_contact, duplicate, or suppressed).
"suppress" — separately counted; the row is suppressed.
campaign_member_import_count = number of clean contacts with crm_action of create_account or update_existing.
Campaign Member Logic (Reconciliation Tasks)
For post-event reconciliation:
- Sponsor contacts with CRM records:
action: "no_action", target_status: "attended_sponsor" (if they attended) or "registered_sponsor" (if registered but didn't attend).
- Qualified non-sponsor badge contacts:
action: "create", target_status: "attended".
- Sponsor contacts without CRM records (badge-only):
action: "create", target_status: "attended_sponsor".
- Excluded badges:
action: "no_import".
Subject key format:
- CRM-resident contacts:
"acct_{account_id}:cont_{contact_id}"
- Badge-only contacts:
"badge:{badge_id}"
Trade Show Prospecting — Ranking & Tiers
Priority Tier Assignment
| Tier |
Criteria |
Opportunity (USD) |
| A |
Requested demo AND interest score ≥ 90 |
120,000 |
| B |
Requested demo AND interest score ≥ 80 |
90,000 |
| C |
All other qualified leads |
50,000 |
Ranking Order (most important first)
requested_demo == true before false
interest_score descending
- Platform coverage count descending (more platforms = higher rank)
company_name ascending (alphabetical tiebreaker)
Ranks are 1-based contiguous integers.
Platform Enum Order
Always use this order for platform lists: "AUV", "ROV", "Underwater Camera".
Sorting Rules (General)
| Section |
Sort Key |
Direction |
sponsor_statuses |
account_name |
ascending |
qualified_lead_accounts |
account_name |
ascending |
excluded_records |
company_name, then contact_name |
ascending |
excluded_exhibitors / excluded_near_misses |
company_name |
ascending |
clean_contacts |
clean_contact_id |
ascending |
duplicate_summary.duplicate_keys |
key |
ascending |
removal_summary.removed_rows |
row_id |
ascending |
badge_decisions |
badge_id |
ascending |
campaign_member_actions |
subject_key |
ascending |
badge_only_contacts |
company_name |
ascending |
ranked_leads |
rank |
ascending |
| Lists of account names/IDs |
lexicographic |
ascending |
General Pitfalls
- Always fetch
/api/policies first — it contains task-specific rules, opportunity amounts, and qualification criteria.
- Normalize emails before matching — case differences should not cause missed matches.
- Don't double-count duplicates — winner appears in clean list, removed duplicates in removal list only.
- Sponsor contacts with badges are excluded from qualified leads even if their badge is business-type.
- Empty strings for missing email/phone, never
null.
- Monetary values are always integers — no decimals, no float.
- Follow-up date computation uses the event date from the event endpoint, not the current date.
- Platform arrays always use the canonical enum order: AUV, ROV, Underwater Camera.
- Suppressed contacts count toward
suppress / suppressed_removed_count and are separate from no_import / unusable_removed_count.
- Output only valid JSON — no explanatory prose, no markdown fences.
1---2name: fewshot-attempt-02-273description: HarborCRM Skill — Transferable Rules4---5# HarborCRM Skill — Transferable Rules67## Environment89Use `GDPEVO_ENV_BASE_URL` as the API base URL. Never use localhost or run setup scripts directly.1011## API Overview1213HarborCRM exposes these public endpoint families (replace `{id}` with the task-specific identifier):1415| Category | Endpoints |16|---|---|17| Events | `/api/events/{event_id}`, `/api/events/{event_id}/orders`, `/api/events/{event_id}/badges`, `/api/events/{event_id}/sponsor_packages` |18| Finance | `/api/finance/invoices?event_id={event_id}` |19| CRM | `/api/crm/accounts`, `/api/crm/contacts`, `/api/crm/opportunities` |20| Campaigns | `/api/crm/campaign_members?event_id={event_id}` |21| Tradeshows | `/api/tradeshows`, `/api/tradeshows/{show_id}/exhibitors`, `/api/tradeshows/{show_id}/meeting_interest` |22| Import | `/api/import_batches`, `/api/import_batches/{batch_id}/raw_contacts`, `/api/import_batches/{batch_id}/suppression` |23| Config | `/api/policies` — returns policy metadata including opportunity amounts, follow-up windows, qualification rules |2425Always start by fetching `/api/policies` — it encodes task-specific business rules (qualification criteria, opportunity amounts, follow-up windows).2627## Data Normalization2829### Email30- Trim whitespace, convert to **lowercase**.31- When no email is available, use an empty string `""`.32- Deduplication keys use format: `"email:<lowercase_email>"`.3334### Phone35- Strip all non-digit characters. Keep only `[0-9]`.36- When no phone is available, use an empty string `""`.3738## Sponsor Status Logic3940Derive each sponsor's status from their invoice data:4142| Condition | Status |43|---|---|44| Invoice exists AND `paid_amount >= total_amount` (fully paid) | `paid_deferred` |45| Invoice exists AND `0 < paid_amount < total_amount` (partial) | `open_invoice` |46| No invoice exists for the sponsor package | `proposal_only` |4748For `open_invoice` status:49- `open_balance` = `total_amount - paid_amount`50- For `paid_deferred` and `proposal_only`, `open_balance` = `0`.5152All monetary values are **integers in USD**.5354## Sponsor Revenue Totals5556Sum package amounts by status:57- `paid_deferred`: sum of all `paid_deferred` sponsor package amounts58- `open_invoice`: sum of all `open_invoice` sponsor package amounts59- `proposal_only`: sum of all `proposal_only` sponsor package amounts60- `open_invoice_balance`: sum of all open balances from `open_invoice` sponsors6162## Qualified Lead / Account Logic6364### Qualification (Event/Exhibitor Tasks)65- A contact is a **qualified non-sponsor lead** if ALL of:66 1. Their company is NOT a sponsor (sponsor attendees are excluded).67 2. Their CRM account is NOT already disqualified.68 3. Their badge is a business badge (not non-business like student/press).69 4. For trade-show tasks: the exhibitor meets the platform/product criteria in the policy (e.g., makes/OEM-builds the target product category).7071### CRM Action Determination72- For each qualified lead, check `/api/crm/accounts` and `/api/crm/contacts`:73 - If a matching CRM account exists → `crm_account_action: "update_existing"`, set `account_id`.74 - If no matching CRM account → `crm_account_action: "create_account"`, `account_id: null`.75 - If a matching CRM contact exists for that account → `crm_contact_action: "update_existing"`, set contact ID.76 - If no matching CRM contact → `crm_contact_action: "create_contact"`.77- Campaign member action for qualified leads is always `"add_campaign_member"`.7879### Opportunity Amount80- Use the event's `lead_opportunity_amount` (from `/api/events/{id}` or `/api/policies`) for each qualified non-sponsor account.81- `lead_pipeline_total` = sum of opportunity amounts across all qualified lead accounts.8283## Exclusion Categories8485| Reason | When to apply |86|---|---|87| `sponsor_attendee` | Contact's company is an active event sponsor |88| `existing_disqualified` | CRM account exists and is already disqualified |89| `non_business_badge` | Badge type is non-business (student, press, speaker, etc.) |90| `inactive_sponsor_record` | Sponsor record is canceled/inactive |91| `distributor_only` | Exhibitor is a distributor, not a manufacturer/OEM |92| `service_only` | Exhibitor provides services only |93| `sensor_vendor_only` / `sensor_only` | Exhibitor sells sensors but not the target platform |94| `research_only` | Exhibitor is a research institution |95| `not_target_market` | Exhibitor doesn't serve the target market |96| `missing_contact` | Row has no contact name |9798Use the "relationship_type" field from the exhibitor data to determine the matching exclusion reason.99100## Deduplication101102When processing import batches:103- Duplicate key = `"email:<normalized_lowercase_email>"`.104- **Winner**: the row with the **lowest `row_id`** (lexicographic/numeric ascending) among duplicates.105- All other rows for that key are marked as `"duplicate"` with reason and go into `removed_rows`.106- `duplicate_removed_count` = total number of removed duplicate rows (not including the winner).107108## Suppression109110From `/api/import_batches/{batch_id}/suppression`:111- Any raw contact whose normalized email matches a suppression entry is marked `crm_action: "suppress"` (or `"no_import"`), reason `"suppressed"`, and added to `removed_rows`.112- Suppressed contacts do NOT appear in `clean_contacts`.113114## Follow-Up Due Dates115116Compute from the event date (from `/api/events/{id}`):117- **Lead follow-up due date**: event date + 90 days.118- **Sponsor finance due date**: event date + 87 days.119- Format: `"YYYY-MM-DD"`.120121### Follow-Up Task Counts122- `lead_task_count` = number of `qualified_lead_accounts`.123- `sponsor_finance_task_count` = number of sponsor accounts with `open_invoice` or `proposal_only` status (unpaid sponsors).124- `sponsor_finance_accounts` = list of account names for those unpaid sponsors, sorted alphabetically ascending.125126## CRM Action Counts127128Count across all qualified lead accounts:129- `accounts_create`: count where `crm_account_action == "create_account"`.130- `accounts_update`: count where `crm_account_action == "update_existing"`.131- `contacts_create`: count where `crm_contact_action == "create_contact"`.132- `contacts_update`: count where `crm_contact_action == "update_existing"`.133- `campaign_members_create`: count of `"add_campaign_member"` actions (= number of qualified leads).134- `campaign_members_update`: count where existing campaign members are updated (typically 0 in fresh handoffs).135136## Import Batch CRM Actions137138For batch import tasks, `crm_action` per clean contact:139- `"create_account"` — company not in CRM, valid contact data.140- `"update_existing"` — company found in CRM (match by account name).141- `"no_import"` — removed (missing_contact, duplicate, or suppressed).142- `"suppress"` — separately counted; the row is suppressed.143144`campaign_member_import_count` = number of clean contacts with `crm_action` of `create_account` or `update_existing`.145146## Campaign Member Logic (Reconciliation Tasks)147148For post-event reconciliation:149- **Sponsor contacts with CRM records**: `action: "no_action"`, `target_status: "attended_sponsor"` (if they attended) or `"registered_sponsor"` (if registered but didn't attend).150- **Qualified non-sponsor badge contacts**: `action: "create"`, `target_status: "attended"`.151- **Sponsor contacts without CRM records** (badge-only): `action: "create"`, `target_status: "attended_sponsor"`.152- **Excluded badges**: `action: "no_import"`.153154Subject key format:155- CRM-resident contacts: `"acct_{account_id}:cont_{contact_id}"`156- Badge-only contacts: `"badge:{badge_id}"`157158## Trade Show Prospecting — Ranking & Tiers159160### Priority Tier Assignment161| Tier | Criteria | Opportunity (USD) |162|---|---|---|163| A | Requested demo AND interest score ≥ 90 | 120,000 |164| B | Requested demo AND interest score ≥ 80 | 90,000 |165| C | All other qualified leads | 50,000 |166167### Ranking Order (most important first)1681. `requested_demo == true` before `false`1692. `interest_score` descending1703. Platform coverage count descending (more platforms = higher rank)1714. `company_name` ascending (alphabetical tiebreaker)172173Ranks are 1-based contiguous integers.174175### Platform Enum Order176Always use this order for platform lists: `"AUV"`, `"ROV"`, `"Underwater Camera"`.177178## Sorting Rules (General)179180| Section | Sort Key | Direction |181|---|---|---|182| `sponsor_statuses` | `account_name` | ascending |183| `qualified_lead_accounts` | `account_name` | ascending |184| `excluded_records` | `company_name`, then `contact_name` | ascending |185| `excluded_exhibitors` / `excluded_near_misses` | `company_name` | ascending |186| `clean_contacts` | `clean_contact_id` | ascending |187| `duplicate_summary.duplicate_keys` | `key` | ascending |188| `removal_summary.removed_rows` | `row_id` | ascending |189| `badge_decisions` | `badge_id` | ascending |190| `campaign_member_actions` | `subject_key` | ascending |191| `badge_only_contacts` | `company_name` | ascending |192| `ranked_leads` | `rank` | ascending |193| Lists of account names/IDs | lexicographic | ascending |194195## General Pitfalls1961971. **Always fetch `/api/policies` first** — it contains task-specific rules, opportunity amounts, and qualification criteria.1982. **Normalize emails before matching** — case differences should not cause missed matches.1993. **Don't double-count duplicates** — winner appears in clean list, removed duplicates in removal list only.2004. **Sponsor contacts with badges are excluded from qualified leads** even if their badge is business-type.2015. **Empty strings for missing email/phone**, never `null`.2026. **Monetary values are always integers** — no decimals, no float.2037. **Follow-up date computation** uses the event date from the event endpoint, not the current date.2048. **Platform arrays** always use the canonical enum order: AUV, ROV, Underwater Camera.2059. **Suppressed contacts** count toward `suppress` / `suppressed_removed_count` and are separate from `no_import` / `unusable_removed_count`.20610. **Output only valid JSON** — no explanatory prose, no markdown fences.