HarborCRM Skill
Environment
All API calls target the base URL supplied by the runner (e.g. http://34.46.77.124:8001). Do not use localhost or run setup scripts unless the remote URL itself points there.
API Reference
| Purpose |
Endpoint |
| Event details |
GET /api/events/{event_id} |
| Sponsor orders |
GET /api/events/{event_id}/orders |
| Sponsor packages |
GET /api/events/{event_id}/sponsor_packages |
| Badge scans |
GET /api/events/{event_id}/badges |
| Finance invoices |
GET /api/finance/invoices?event_id={event_id} |
| CRM accounts |
GET /api/crm/accounts |
| CRM contacts |
GET /api/crm/contacts |
| CRM opportunities |
GET /api/crm/opportunities |
| Campaign members |
GET /api/crm/campaign_members?event_id={event_id} |
| Policies |
GET /api/policies |
| Tradeshow list |
GET /api/tradeshows |
| Tradeshow exhibitors |
GET /api/tradeshows/{show_id}/exhibitors |
| Meeting interest |
GET /api/tradeshows/{show_id}/meeting_interest |
| Import batches |
GET /api/import_batches |
| Raw import contacts |
GET /api/import_batches/{batch_id}/raw_contacts |
| Suppression list |
GET /api/import_batches/{batch_id}/suppression |
Contact Normalization
- Email: lowercase, trimmed. Use empty string
"" when no email is supplied.
- Phone: digits only. Preserve country code when present in the source (e.g.
+1 415-555-0188 → 14155550188). Use empty string when no phone is supplied.
Sponsor Reconciliation (Event Tasks)
Determining Sponsor Status
Cross-reference orders and invoices. Map to the controlled status values:
| Condition |
Status |
Invoice with status paid_deferred |
paid_deferred |
Invoice with status open |
open_invoice |
| Order exists, no invoice |
proposal_only |
Order status canceled |
Exclude from active sponsors |
Only active (non-canceled) sponsor orders appear in sponsor statuses. Include invoice_id as null for proposal-only sponsors.
Sponsor Revenue
sponsor_revenue_totals: sum package_amount (from order/invoice) by status.
open_invoice_balance: sum of amount - paid_amount across open invoices.
open_balance per sponsor: amount - paid_amount.
Sponsor Follow-Up
sponsor_finance_due_date: event end_date + sponsor_followup_days_after_end.
- Unpaid sponsors include both open_invoice AND proposal_only sponsors.
unpaid_sponsor_total_usd: sum of their amounts.
Lead Qualification (Event Tasks)
Inclusion Rules
A badge qualifies as a lead when ALL of the following hold:
- Badge type is not
sponsor, student, press, or other non-business type.
- The contact is not listed as a
ticket_contact on any sponsor order.
- The company's CRM account is not disqualified (status ≠
disqualified, disqualified_reason is null).
Exclusion Categories
| Condition |
Exclusion Reason |
| Sponsor badge or sponsor ticket contact |
sponsor_attendee |
| Badge type is student/press/etc. |
non_business_badge |
| CRM account is disqualified |
existing_disqualified |
| Canceled sponsor order's contacts |
sponsor_attendee |
- Contacts with empty email but a valid phone are still qualified (not
missing_contact).
- Each badge gets one classification:
sponsor_attendee, qualified_non_sponsor_lead, or excluded.
Opportunity Amounts
- Use the event's
lead_opportunity_amount for every qualified non-sponsor lead account.
lead_pipeline_total = sum of all qualified lead opportunity amounts.
CRM Actions per Lead
- Account exists in CRM →
update_existing. No CRM account → create_account.
- Contact exists in CRM →
update_existing (account-level). New contact → create_contact.
- Campaign member:
add_campaign_member for all qualified leads.
CRM Account Matching
Match badge company to CRM account by:
- Exact
account_id match (if present on badge).
- Email domain match (badge email domain == CRM account
domain).
- Name match (fuzzy — normalize case and whitespace).
Follow-Up Dates
lead_due_date: event end_date + followup_days_after_end.
sponsor_finance_due_date: event end_date + sponsor_followup_days_after_end.
Campaign Member Actions
For event reconciliation tasks, build campaign_member_actions covering:
- Every badge holder.
- Every existing campaign member for the event (even those without a badge).
Target status mapping:
| Condition |
Target Status |
| Sponsor who attended (has badge) |
attended_sponsor |
| Sponsor who registered only (no badge) |
registered_sponsor |
| Non-sponsor attendee (qualified lead) |
attended |
| Excluded (non-business, disqualified, etc.) |
excluded |
Action mapping:
| Condition |
Action |
| Campaign member already exists, status matches target |
no_action |
| Campaign member needed, doesn't exist |
create |
| Excluded, no campaign member needed |
no_import |
Use subject_key as a unique identifier (badge_id or account_id), sorted ascending.
Prospecting (Tradeshow Tasks)
Qualification
An exhibitor is qualified IFF it manufactures or OEM-builds platforms covered by the campaign (AUV, ROV, Underwater Camera). Determine this from the exhibitor description field. Be thorough: if a description mentions both an ROV and cameras, include both platforms.
Exclusion Reasons
| Exhibitor Profile |
exclusion_reason |
relationship_type |
| Reseller/distributor, no manufacturing |
distributor_only |
distributor |
| Consulting/analytics service, no hardware |
service_only |
service_provider |
| Sensor component vendor only, no platform |
sensor_vendor_only |
sensor_vendor |
| Research institution, no commercial product |
research_only |
research |
| Doesn't fit target market |
not_target_market |
— |
Every exhibitor must be classified — none left unaccounted for.
Platform Classification
Allowed platforms: AUV, ROV, Underwater Camera. List in this enum order. Only include platforms the exhibitor actually builds; parse descriptions carefully.
Priority Tiers & Opportunity Sizing
| Condition |
Tier |
Opportunity |
| Requested demo AND interest score ≥ 90 |
A |
$120,000 |
| Requested demo AND interest score ≥ 80 |
B |
$90,000 |
| All other qualified leads |
C |
$50,000 |
Ranking
Apply sort keys in order:
- Demo requested first (
true before false).
- Interest score descending (higher first).
- Broader platform coverage (more platforms first).
- Company name ascending (alphabetical).
Assign 1-based contiguous rank integers.
CRM Actions
- Exhibitor has a non-null
crm_account_id matching a CRM account → update_existing.
- No CRM account →
create_account.
Summary
existing_crm_overlap_count: count of qualified leads with existing CRM accounts.
existing_crm_overlap_account_ids: those account IDs, sorted ascending.
platform_coverage_counts: count of qualified leads per platform type.
total_estimated_opportunity_usd: sum of all qualified lead opportunity estimates.
Import Batch Processing
Workflow
- Fetch
raw_contacts and suppression list.
- Dedup: identify rows with the same normalized email. Keep the winning row, remove the rest.
- Winner selection: prefer
partner_upload source over webinar_form. When sources are equal, prefer the later captured_at timestamp.
- Dedup key: normalized email (lowercase, trimmed).
- Remove unusable: rows with whitespace-only/empty email AND no phone →
missing_contact.
- Suppress: match by email against suppression list →
suppressed.
- Suppressed contacts are removed from clean_contacts and counted in
removal_summary.suppressed_removed_count. They do not appear in import_action_totals.suppress (that field stays 0).
CRM Matching for Import
- Match email domain against CRM account
domain to find existing_account_id.
- Match normalized email against CRM contact
email to find existing_contact_id.
- Account exists →
crm_action: "update_existing". No account → crm_action: "create_account".
Output Conventions
clean_contact_id = winning row's row_id.
source_row_id = winning row's row_id.
clean_contacts sorted by clean_contact_id ascending.
duplicate_keys sorted by key ascending.
removed_rows sorted by row_id ascending.
campaign_member_import_count: count of clean contacts with crm_action create_account or update_existing.
Sorting Rules Summary
| List |
Sort Key |
Direction |
| sponsor_statuses |
account_name |
ascending |
| qualified_lead_accounts |
account_name |
ascending |
| excluded_records |
company_name, then contact_name |
ascending |
| badge_decisions |
badge_id |
ascending |
| campaign_member_actions |
subject_key |
ascending |
| badge_only_contacts |
company_name |
ascending |
| qualified_exhibitors |
company_name |
ascending |
| excluded_near_misses / excluded_exhibitors |
company_name |
ascending |
| ranked_leads |
rank |
ascending |
| clean_contacts |
clean_contact_id |
ascending |
| platforms (within an item) |
enum order: AUV, ROV, Underwater Camera |
— |
Data Types & Precision
- Currency: integer USD (no decimals).
- Counts: integers.
- Dates:
YYYY-MM-DD strings.
- Timestamps: ISO 8601 strings as returned by the API.
- Nulls: use JSON
null (not the string "null").
Common Pitfalls
- Canceled sponsors: Do not include canceled orders in active sponsor statuses, but their contacts still appear as
sponsor_attendee exclusions.
- Proposal-only sponsors: Count them in unpaid sponsor follow-up and sponsor statuses, but they have no invoice (invoice_id = null, paid = 0).
- Platform detection: Read exhibitor descriptions holistically — "camera arrays" on an ROV counts as both ROV and Underwater Camera platforms.
- Empty vs missing: Empty email string
"" is NOT the same as missing contact. A badge with phone but no email is still a valid lead.
- Suppressed vs clean: Suppressed contacts are REMOVED from clean_contacts entirely; do not list them with
crm_action: "suppress".
- Source priority for dedup:
partner_upload beats webinar_form. When sources match, later captured_at wins.
- CRM matching: Match by email domain, not company name. Company names in raw data can be abbreviations (e.g. "HelioWare Mfg." vs "HelioWare Manufacturing").
- Duplicates: Dedup key is normalized email only. Same email = same person regardless of name/phone variation.
- All entities accounted: Every badge, exhibitor, or raw contact must appear in exactly one output list — never leave an entity unclassified.
- Template fidelity: Do not add extra fields beyond the answer template. Use only the controlled enum values specified.
1---2name: reflect-3-attempt-01-413description: HarborCRM Skill4---5# HarborCRM Skill67## Environment89All API calls target the base URL supplied by the runner (e.g. `http://34.46.77.124:8001`). Do not use localhost or run setup scripts unless the remote URL itself points there.1011## API Reference1213| Purpose | Endpoint |14|---|---|15| Event details | `GET /api/events/{event_id}` |16| Sponsor orders | `GET /api/events/{event_id}/orders` |17| Sponsor packages | `GET /api/events/{event_id}/sponsor_packages` |18| Badge scans | `GET /api/events/{event_id}/badges` |19| Finance invoices | `GET /api/finance/invoices?event_id={event_id}` |20| CRM accounts | `GET /api/crm/accounts` |21| CRM contacts | `GET /api/crm/contacts` |22| CRM opportunities | `GET /api/crm/opportunities` |23| Campaign members | `GET /api/crm/campaign_members?event_id={event_id}` |24| Policies | `GET /api/policies` |25| Tradeshow list | `GET /api/tradeshows` |26| Tradeshow exhibitors | `GET /api/tradeshows/{show_id}/exhibitors` |27| Meeting interest | `GET /api/tradeshows/{show_id}/meeting_interest` |28| Import batches | `GET /api/import_batches` |29| Raw import contacts | `GET /api/import_batches/{batch_id}/raw_contacts` |30| Suppression list | `GET /api/import_batches/{batch_id}/suppression` |3132## Contact Normalization3334- **Email**: lowercase, trimmed. Use empty string `""` when no email is supplied.35- **Phone**: digits only. Preserve country code when present in the source (e.g. `+1 415-555-0188` → `14155550188`). Use empty string when no phone is supplied.3637## Sponsor Reconciliation (Event Tasks)3839### Determining Sponsor Status4041Cross-reference orders and invoices. Map to the controlled status values:4243| Condition | Status |44|---|---|45| Invoice with status `paid_deferred` | `paid_deferred` |46| Invoice with status `open` | `open_invoice` |47| Order exists, no invoice | `proposal_only` |48| Order status `canceled` | Exclude from active sponsors |4950Only **active** (non-canceled) sponsor orders appear in sponsor statuses. Include `invoice_id` as `null` for proposal-only sponsors.5152### Sponsor Revenue5354- `sponsor_revenue_totals`: sum `package_amount` (from order/invoice) by status.55- `open_invoice_balance`: sum of `amount - paid_amount` across open invoices.56- `open_balance` per sponsor: `amount - paid_amount`.5758### Sponsor Follow-Up5960- `sponsor_finance_due_date`: event `end_date` + `sponsor_followup_days_after_end`.61- Unpaid sponsors include both **open_invoice** AND **proposal_only** sponsors.62- `unpaid_sponsor_total_usd`: sum of their amounts.6364## Lead Qualification (Event Tasks)6566### Inclusion Rules67A badge qualifies as a lead when ALL of the following hold:681. Badge type is **not** `sponsor`, `student`, `press`, or other non-business type.692. The contact is **not** listed as a `ticket_contact` on any sponsor order.703. The company's CRM account is **not** disqualified (status ≠ `disqualified`, `disqualified_reason` is null).7172### Exclusion Categories7374| Condition | Exclusion Reason |75|---|---|76| Sponsor badge or sponsor ticket contact | `sponsor_attendee` |77| Badge type is student/press/etc. | `non_business_badge` |78| CRM account is disqualified | `existing_disqualified` |79| Canceled sponsor order's contacts | `sponsor_attendee` |8081- Contacts with empty email but a valid phone are **still qualified** (not `missing_contact`).82- Each badge gets one classification: `sponsor_attendee`, `qualified_non_sponsor_lead`, or `excluded`.8384### Opportunity Amounts85- Use the event's `lead_opportunity_amount` for every qualified non-sponsor lead account.86- `lead_pipeline_total` = sum of all qualified lead opportunity amounts.8788### CRM Actions per Lead89- Account exists in CRM → `update_existing`. No CRM account → `create_account`.90- Contact exists in CRM → `update_existing` (account-level). New contact → `create_contact`.91- Campaign member: `add_campaign_member` for all qualified leads.9293### CRM Account Matching94Match badge company to CRM account by:951. Exact `account_id` match (if present on badge).962. Email domain match (badge email domain == CRM account `domain`).973. Name match (fuzzy — normalize case and whitespace).9899### Follow-Up Dates100- `lead_due_date`: event `end_date` + `followup_days_after_end`.101- `sponsor_finance_due_date`: event `end_date` + `sponsor_followup_days_after_end`.102103## Campaign Member Actions104105For event reconciliation tasks, build `campaign_member_actions` covering:106- Every badge holder.107- Every existing campaign member for the event (even those without a badge).108109**Target status mapping:**110| Condition | Target Status |111|---|---|112| Sponsor who attended (has badge) | `attended_sponsor` |113| Sponsor who registered only (no badge) | `registered_sponsor` |114| Non-sponsor attendee (qualified lead) | `attended` |115| Excluded (non-business, disqualified, etc.) | `excluded` |116117**Action mapping:**118| Condition | Action |119|---|---|120| Campaign member already exists, status matches target | `no_action` |121| Campaign member needed, doesn't exist | `create` |122| Excluded, no campaign member needed | `no_import` |123124Use `subject_key` as a unique identifier (badge_id or account_id), sorted ascending.125126## Prospecting (Tradeshow Tasks)127128### Qualification129An exhibitor is qualified IFF it **manufactures or OEM-builds** platforms covered by the campaign (AUV, ROV, Underwater Camera). Determine this from the exhibitor `description` field. Be thorough: if a description mentions both an ROV and cameras, include both platforms.130131### Exclusion Reasons132133| Exhibitor Profile | `exclusion_reason` | `relationship_type` |134|---|---|---|135| Reseller/distributor, no manufacturing | `distributor_only` | `distributor` |136| Consulting/analytics service, no hardware | `service_only` | `service_provider` |137| Sensor component vendor only, no platform | `sensor_vendor_only` | `sensor_vendor` |138| Research institution, no commercial product | `research_only` | `research` |139| Doesn't fit target market | `not_target_market` | — |140141Every exhibitor must be classified — none left unaccounted for.142143### Platform Classification144Allowed platforms: `AUV`, `ROV`, `Underwater Camera`. List in this enum order. Only include platforms the exhibitor actually builds; parse descriptions carefully.145146### Priority Tiers & Opportunity Sizing147148| Condition | Tier | Opportunity |149|---|---|---|150| Requested demo AND interest score ≥ 90 | A | $120,000 |151| Requested demo AND interest score ≥ 80 | B | $90,000 |152| All other qualified leads | C | $50,000 |153154### Ranking155Apply sort keys in order:1561. **Demo requested** first (`true` before `false`).1572. **Interest score** descending (higher first).1583. **Broader platform coverage** (more platforms first).1594. **Company name** ascending (alphabetical).160161Assign 1-based contiguous rank integers.162163### CRM Actions164- Exhibitor has a non-null `crm_account_id` matching a CRM account → `update_existing`.165- No CRM account → `create_account`.166167### Summary168- `existing_crm_overlap_count`: count of qualified leads with existing CRM accounts.169- `existing_crm_overlap_account_ids`: those account IDs, sorted ascending.170- `platform_coverage_counts`: count of qualified leads per platform type.171- `total_estimated_opportunity_usd`: sum of all qualified lead opportunity estimates.172173## Import Batch Processing174175### Workflow1761. Fetch `raw_contacts` and `suppression` list.1772. **Dedup**: identify rows with the same normalized email. Keep the winning row, remove the rest.178 - **Winner selection**: prefer `partner_upload` source over `webinar_form`. When sources are equal, prefer the **later** `captured_at` timestamp.179 - **Dedup key**: normalized email (lowercase, trimmed).1803. **Remove unusable**: rows with whitespace-only/empty email AND no phone → `missing_contact`.1814. **Suppress**: match by email against suppression list → `suppressed`.182 - Suppressed contacts are **removed** from clean_contacts and counted in `removal_summary.suppressed_removed_count`. They do **not** appear in `import_action_totals.suppress` (that field stays `0`).183184### CRM Matching for Import185- Match email domain against CRM account `domain` to find `existing_account_id`.186- Match normalized email against CRM contact `email` to find `existing_contact_id`.187- Account exists → `crm_action: "update_existing"`. No account → `crm_action: "create_account"`.188189### Output Conventions190- `clean_contact_id` = winning row's `row_id`.191- `source_row_id` = winning row's `row_id`.192- `clean_contacts` sorted by `clean_contact_id` ascending.193- `duplicate_keys` sorted by `key` ascending.194- `removed_rows` sorted by `row_id` ascending.195- `campaign_member_import_count`: count of clean contacts with crm_action `create_account` or `update_existing`.196197## Sorting Rules Summary198199| List | Sort Key | Direction |200|---|---|---|201| sponsor_statuses | account_name | ascending |202| qualified_lead_accounts | account_name | ascending |203| excluded_records | company_name, then contact_name | ascending |204| badge_decisions | badge_id | ascending |205| campaign_member_actions | subject_key | ascending |206| badge_only_contacts | company_name | ascending |207| qualified_exhibitors | company_name | ascending |208| excluded_near_misses / excluded_exhibitors | company_name | ascending |209| ranked_leads | rank | ascending |210| clean_contacts | clean_contact_id | ascending |211| platforms (within an item) | enum order: AUV, ROV, Underwater Camera | — |212213## Data Types & Precision214215- **Currency**: integer USD (no decimals).216- **Counts**: integers.217- **Dates**: `YYYY-MM-DD` strings.218- **Timestamps**: ISO 8601 strings as returned by the API.219- **Nulls**: use JSON `null` (not the string `"null"`).220221## Common Pitfalls2222231. **Canceled sponsors**: Do not include canceled orders in active sponsor statuses, but their contacts still appear as `sponsor_attendee` exclusions.2242. **Proposal-only sponsors**: Count them in unpaid sponsor follow-up and sponsor statuses, but they have no invoice (invoice_id = null, paid = 0).2253. **Platform detection**: Read exhibitor descriptions holistically — "camera arrays" on an ROV counts as both ROV and Underwater Camera platforms.2264. **Empty vs missing**: Empty email string `""` is NOT the same as missing contact. A badge with phone but no email is still a valid lead.2275. **Suppressed vs clean**: Suppressed contacts are REMOVED from clean_contacts entirely; do not list them with `crm_action: "suppress"`.2286. **Source priority for dedup**: `partner_upload` beats `webinar_form`. When sources match, later `captured_at` wins.2297. **CRM matching**: Match by email domain, not company name. Company names in raw data can be abbreviations (e.g. "HelioWare Mfg." vs "HelioWare Manufacturing").2308. **Duplicates**: Dedup key is normalized email only. Same email = same person regardless of name/phone variation.2319. **All entities accounted**: Every badge, exhibitor, or raw contact must appear in exactly one output list — never leave an entity unclassified.23210. **Template fidelity**: Do not add extra fields beyond the answer template. Use only the controlled enum values specified.