HarborCRM Skill
Environment
Base URL: use the API base URL supplied by the runner. All endpoints are relative to this base. Do not start local services or run setup scripts.
API Endpoints by Domain
Events
GET /api/events/{event_id} — event metadata (dates, follow-up offsets, lead opportunity amount, campaign code)
GET /api/events/{event_id}/orders — sponsor orders (account_id, amount, order_status, package_level, ticket_contacts)
GET /api/events/{event_id}/badges — scanned badges (badge_type, company_name, contact_name, email, phone, scan_score)
GET /api/events/{event_id}/sponsor_packages — same shape as orders
Finance
GET /api/finance/invoices?event_id={event_id} — invoices (amount, paid_amount, deferred_amount, status, due_date, payment_date)
CRM
GET /api/crm/accounts — all CRM accounts (account_id, name, domain, status, disqualified_reason)
GET /api/crm/contacts — all CRM contacts (account_id, contact_id, email, name, phone, opted_out)
GET /api/crm/opportunities — all opportunities (account_id, amount, stage, event_id)
GET /api/crm/campaign_members?event_id={event_id} — event campaign members (account_id, contact_id, status, last_activity_date)
Tradeshows
GET /api/tradeshows — list all shows
GET /api/tradeshows/{show_id}/exhibitors — exhibitors with company_id, description, booth, country, website, crm_account_id
GET /api/tradeshows/{show_id}/meeting_interest — demo requests and interest scores
Import Batches
GET /api/import_batches — list batches
GET /api/import_batches/{batch_id}/raw_contacts — raw contact rows
GET /api/import_batches/{batch_id}/suppression — suppression list (email, phone, reason)
Policies
GET /api/policies — platform enums, qualification notes, sponsor status enums
Sponsor Status Rules
Assign one of: paid_deferred, open_invoice, proposal_only, not_sponsor.
| Condition |
Status |
Invoice exists with status: "paid_deferred" (fully paid) |
paid_deferred |
Invoice exists with status: "open" (balance remaining) |
open_invoice |
| Sponsor order exists but no invoice |
proposal_only |
Order order_status: "canceled" |
exclude from sponsor list (inactive) |
open_balance = amount - paid_amount for each sponsor line.
- For
paid_deferred, open_balance is 0, paid_amount is full amount.
- For
proposal_only, invoice_id is null, paid_amount is 0, open_balance is 0.
- Revenue totals aggregate by status.
open_invoice_balance sums open balances of open-invoice sponsors.
Badge Classification (Event Reconciliation Tasks)
Each badge gets one classification and one crm_action:
| Badge type / Company status |
Classification |
crm_action |
exclusion_reason |
badge_type: "sponsor" |
sponsor_attendee |
no_import |
sponsor_attendee |
| Company has active sponsor order (any badge type) |
sponsor_attendee |
no_import |
sponsor_attendee |
badge_type is student, press, vendor_staff, etc. |
excluded |
no_import |
non_business_badge |
CRM account status: "disqualified" |
excluded |
no_import |
existing_disqualified |
Sponsor order order_status: "canceled" |
excluded |
no_import |
inactive_sponsor_record |
| Missing contact_name (empty/whitespace only) |
excluded |
no_import |
missing_contact |
| Attendee badge, company not a sponsor, account not disqualified |
qualified_non_sponsor_lead |
create_account_contact_campaign_member |
null |
For qualified non-sponsor leads where the CRM account already exists, use create_contact_campaign_member instead of create_account_contact_campaign_member.
Apply exclusions in priority order: sponsor → inactive sponsor → non-business badge → disqualified → missing contact. The first matching exclusion is used.
Campaign Member Actions (Event Reconciliation)
Create one entry per badge contact plus existing campaign members that need action:
subject_key: "{account_name} | {contact_name}" (accessible sort key)
action: create (new), no_action (existing, correct status), no_import (excluded)
target_status: attended_sponsor for sponsor attendees, attended for qualified leads, registered_sponsor for sponsor contacts without a badge scan, excluded for excluded badges
Existing campaign members whose status is already correct get action: "no_action".
Tradeshow Prospecting Qualification
Platform Assignment
Read the exhibitor description field. Assign platforms from the policy enums: AUV, ROV, Underwater Camera.
- AUV: builds/manufactures autonomous underwater vehicles
- ROV: builds/manufactures remotely operated vehicles
- Underwater Camera: builds/OEM-manufactures underwater camera hardware
Companies that ONLY distribute, resell, provide services, make sensors (not platforms), or do research/analytics without manufacturing hardware are excluded.
Exclusion Reasons (controlled enum)
| Business type |
exclusion_reason |
| Distributor/reseller, no manufacturing |
distributor_only |
| Service/consulting/analytics, no hardware build |
service_only |
| Sensor-only vendor, no platform build |
sensor_vendor_only |
| Research-only, no product build |
research_only |
| Does not serve target market |
not_target_market |
For tasks using relationship_type + exclusion_reason pairs: map distributor → distributor_only, service_provider → service_only, sensor_vendor → sensor_only, research → research_only.
Priority Tier Assignment
When explicit tier rules are provided in the task prompt, follow them. The default pattern (used when rules are not explicit):
- A: demo requested AND interest score ≥ 90 (USD 120,000)
- B: demo requested AND interest score ≥ 80 (USD 90,000)
- C: all other qualified (USD 50,000)
When the task defines specific opportunity amounts per tier, use those amounts. Otherwise, use the event's lead_opportunity_amount.
Ranking
When ranking is required:
- Demo requested first (true before false)
- Interest score descending
- Number of platforms covered descending (broader coverage first)
- Company name ascending (alphabetical tiebreaker)
Ranks are 1-based and contiguous.
Import Batch Cleaning
Contact Normalization
- Email: trim whitespace, convert to lowercase. Empty after trim → empty string.
- Phone: extract digits only. Empty after extraction → empty string.
Deduplication
- Dedup key: normalized email address.
- When multiple rows share the same normalized email, pick one winner.
- Winner selection: prefer the row with earliest
captured_at (first capture wins). On timestamp tie, prefer the lower row_id.
- Use the winner's raw field values (company_name, contact_name, etc.) for the clean contact record.
clean_contact_id = winning row_id. source_row_id = same winning row_id.
Suppression
- Check each contact's normalized email and phone against the suppression list.
- If either matches → contact is suppressed (excluded from import).
Missing Contact
- A row is unusable if
contact_name is empty/whitespace only, OR if both normalized email AND normalized phone are empty.
CRM Matching
- Match account by exact
company_name match against CRM account name.
- Match contact by normalized email against CRM contact
email.
crm_action:
- Account match AND contact match →
update_existing
- Account match, no contact match →
update_existing
- No account match, contact match →
update_existing
- No account match, no contact match →
create_account
- On suppression list →
suppress
- Missing contact →
no_import
existing_account_id: the matched CRM account_id, or null.
existing_contact_id: the matched CRM contact_id, or null.
Clean Contacts
- Include ONLY contacts with
crm_action of create_account or update_existing.
- Suppressed and no_import rows go only in
removal_summary, not in clean_contacts.
Campaign Member Import Count
- Count of contacts in
clean_contacts (the surviving importable contacts).
Follow-Up Dates
- Lead follow-up due date:
end_date + followup_days_after_end (ISO YYYY-MM-DD)
- Sponsor finance due date:
end_date + sponsor_followup_days_after_end (ISO YYYY-MM-DD)
- If
start_date equals end_date (single-day event), use that date as the base.
For events where only one follow-up offset is given, derive both dates from it.
Sorting Rules (apply everywhere)
- Sort strings ascending alphabetically (case-sensitive, standard lexicographic).
- Sort lists of objects by the primary key specified in the template.
- Platform lists: always sort in enum order:
AUV, ROV, Underwater Camera.
- Subject keys in campaign member actions: sort ascending by
subject_key.
- Sponsor statuses and qualified leads: sort by
account_name/company_name ascending.
- Excluded records: sort by
company_name ascending, then contact_name ascending.
- Badge decisions: sort by
badge_id ascending.
- Clean contacts: sort by
clean_contact_id ascending.
- Duplicate keys: sort by
key ascending.
- Removed rows: sort by
row_id ascending.
- CRM account IDs in lists: sort ascending.
Data Type Conventions
- All monetary amounts: integers (USD).
- All counts: integers.
- Boolean fields: JSON
true/false (not strings).
- Nullable fields: JSON
null when no value.
- Empty strings allowed for email/phone when no data was supplied.
- Dates:
YYYY-MM-DD strings. Timestamps: ISO 8601 as returned by the API.
Key Pitfalls
- Company name matching is exact: "HelioWare Manufacturing" ≠ "HelioWare Mfg." in CRM. Use the winning row's exact company name for CRM lookups.
- Sponsor badge ≠ sponsor company: A company with an active sponsor order makes ALL its badge holders sponsor attendees, even if their individual
badge_type is "attendee".
- Canceled ≠ inactive: Only
order_status: "canceled" is inactive. proposal_sent is an active (proposal_only) sponsor.
- Deferred revenue vs open balance:
paid_deferred means fully paid but revenue deferred for accounting — no follow-up needed. open_invoice means money is still owed — needs finance follow-up.
- Platform assignment from descriptions: "Builds compact AUVs" → AUV; "camera modules" → Underwater Camera; "reseller" → no platform. Read descriptions literally; don't over-assign.
- Demo request drives ranking priority: requested_demo=true always sorts before false, regardless of score.
- Suppression check happens during import cleaning: contacts matching the suppression list by email or phone are excluded from import and counted as suppressed in totals.
- Empty email alone is not
missing_contact: a badge with a contact_name and phone but no email is still a valid lead unless the task explicitly requires email.
1---2name: reflect-3-attempt-03-413description: HarborCRM Skill4---5# HarborCRM Skill67## Environment89Base URL: use the API base URL supplied by the runner. All endpoints are relative to this base. Do not start local services or run setup scripts.1011## API Endpoints by Domain1213### Events14- `GET /api/events/{event_id}` — event metadata (dates, follow-up offsets, lead opportunity amount, campaign code)15- `GET /api/events/{event_id}/orders` — sponsor orders (account_id, amount, order_status, package_level, ticket_contacts)16- `GET /api/events/{event_id}/badges` — scanned badges (badge_type, company_name, contact_name, email, phone, scan_score)17- `GET /api/events/{event_id}/sponsor_packages` — same shape as orders1819### Finance20- `GET /api/finance/invoices?event_id={event_id}` — invoices (amount, paid_amount, deferred_amount, status, due_date, payment_date)2122### CRM23- `GET /api/crm/accounts` — all CRM accounts (account_id, name, domain, status, disqualified_reason)24- `GET /api/crm/contacts` — all CRM contacts (account_id, contact_id, email, name, phone, opted_out)25- `GET /api/crm/opportunities` — all opportunities (account_id, amount, stage, event_id)26- `GET /api/crm/campaign_members?event_id={event_id}` — event campaign members (account_id, contact_id, status, last_activity_date)2728### Tradeshows29- `GET /api/tradeshows` — list all shows30- `GET /api/tradeshows/{show_id}/exhibitors` — exhibitors with company_id, description, booth, country, website, crm_account_id31- `GET /api/tradeshows/{show_id}/meeting_interest` — demo requests and interest scores3233### Import Batches34- `GET /api/import_batches` — list batches35- `GET /api/import_batches/{batch_id}/raw_contacts` — raw contact rows36- `GET /api/import_batches/{batch_id}/suppression` — suppression list (email, phone, reason)3738### Policies39- `GET /api/policies` — platform enums, qualification notes, sponsor status enums4041## Sponsor Status Rules4243Assign one of: `paid_deferred`, `open_invoice`, `proposal_only`, `not_sponsor`.4445| Condition | Status |46|---|---|47| Invoice exists with `status: "paid_deferred"` (fully paid) | `paid_deferred` |48| Invoice exists with `status: "open"` (balance remaining) | `open_invoice` |49| Sponsor order exists but no invoice | `proposal_only` |50| Order `order_status: "canceled"` | exclude from sponsor list (inactive) |5152- `open_balance` = `amount - paid_amount` for each sponsor line.53- For `paid_deferred`, `open_balance` is 0, `paid_amount` is full amount.54- For `proposal_only`, `invoice_id` is null, `paid_amount` is 0, `open_balance` is 0.55- Revenue totals aggregate by status. `open_invoice_balance` sums open balances of open-invoice sponsors.5657## Badge Classification (Event Reconciliation Tasks)5859Each badge gets one classification and one crm_action:6061| Badge type / Company status | Classification | crm_action | exclusion_reason |62|---|---|---|---|63| `badge_type: "sponsor"` | `sponsor_attendee` | `no_import` | `sponsor_attendee` |64| Company has active sponsor order (any badge type) | `sponsor_attendee` | `no_import` | `sponsor_attendee` |65| `badge_type` is `student`, `press`, `vendor_staff`, etc. | `excluded` | `no_import` | `non_business_badge` |66| CRM account `status: "disqualified"` | `excluded` | `no_import` | `existing_disqualified` |67| Sponsor order `order_status: "canceled"` | `excluded` | `no_import` | `inactive_sponsor_record` |68| Missing contact_name (empty/whitespace only) | `excluded` | `no_import` | `missing_contact` |69| Attendee badge, company not a sponsor, account not disqualified | `qualified_non_sponsor_lead` | `create_account_contact_campaign_member` | null |7071For qualified non-sponsor leads where the CRM account already exists, use `create_contact_campaign_member` instead of `create_account_contact_campaign_member`.7273Apply exclusions in priority order: sponsor → inactive sponsor → non-business badge → disqualified → missing contact. The first matching exclusion is used.7475## Campaign Member Actions (Event Reconciliation)7677Create one entry per badge contact plus existing campaign members that need action:7879- `subject_key`: `"{account_name} | {contact_name}"` (accessible sort key)80- `action`: `create` (new), `no_action` (existing, correct status), `no_import` (excluded)81- `target_status`: `attended_sponsor` for sponsor attendees, `attended` for qualified leads, `registered_sponsor` for sponsor contacts without a badge scan, `excluded` for excluded badges8283Existing campaign members whose status is already correct get `action: "no_action"`.8485## Tradeshow Prospecting Qualification8687### Platform Assignment88Read the exhibitor `description` field. Assign platforms from the policy enums: `AUV`, `ROV`, `Underwater Camera`.8990- **AUV**: builds/manufactures autonomous underwater vehicles91- **ROV**: builds/manufactures remotely operated vehicles92- **Underwater Camera**: builds/OEM-manufactures underwater camera hardware9394Companies that ONLY distribute, resell, provide services, make sensors (not platforms), or do research/analytics without manufacturing hardware are **excluded**.9596### Exclusion Reasons (controlled enum)97| Business type | `exclusion_reason` |98|---|---|99| Distributor/reseller, no manufacturing | `distributor_only` |100| Service/consulting/analytics, no hardware build | `service_only` |101| Sensor-only vendor, no platform build | `sensor_vendor_only` |102| Research-only, no product build | `research_only` |103| Does not serve target market | `not_target_market` |104105For tasks using `relationship_type` + `exclusion_reason` pairs: map `distributor` → `distributor_only`, `service_provider` → `service_only`, `sensor_vendor` → `sensor_only`, `research` → `research_only`.106107### Priority Tier Assignment108When explicit tier rules are provided in the task prompt, follow them. The default pattern (used when rules are not explicit):109- **A**: demo requested AND interest score ≥ 90 (USD 120,000)110- **B**: demo requested AND interest score ≥ 80 (USD 90,000)111- **C**: all other qualified (USD 50,000)112113When the task defines specific opportunity amounts per tier, use those amounts. Otherwise, use the event's `lead_opportunity_amount`.114115### Ranking116When ranking is required:1171. Demo requested first (true before false)1182. Interest score descending1193. Number of platforms covered descending (broader coverage first)1204. Company name ascending (alphabetical tiebreaker)121122Ranks are 1-based and contiguous.123124## Import Batch Cleaning125126### Contact Normalization127- **Email**: trim whitespace, convert to lowercase. Empty after trim → empty string.128- **Phone**: extract digits only. Empty after extraction → empty string.129130### Deduplication131- **Dedup key**: normalized email address.132- When multiple rows share the same normalized email, pick one winner.133- Winner selection: prefer the row with earliest `captured_at` (first capture wins). On timestamp tie, prefer the lower `row_id`.134- Use the winner's raw field values (company_name, contact_name, etc.) for the clean contact record.135- `clean_contact_id` = winning `row_id`. `source_row_id` = same winning `row_id`.136137### Suppression138- Check each contact's normalized email and phone against the suppression list.139- If either matches → contact is suppressed (excluded from import).140141### Missing Contact142- A row is unusable if `contact_name` is empty/whitespace only, OR if both normalized email AND normalized phone are empty.143144### CRM Matching145- Match account by exact `company_name` match against CRM account `name`.146- Match contact by normalized email against CRM contact `email`.147- `crm_action`:148 - Account match AND contact match → `update_existing`149 - Account match, no contact match → `update_existing`150 - No account match, contact match → `update_existing`151 - No account match, no contact match → `create_account`152 - On suppression list → `suppress`153 - Missing contact → `no_import`154- `existing_account_id`: the matched CRM `account_id`, or null.155- `existing_contact_id`: the matched CRM `contact_id`, or null.156157### Clean Contacts158- Include ONLY contacts with `crm_action` of `create_account` or `update_existing`.159- Suppressed and no_import rows go only in `removal_summary`, not in `clean_contacts`.160161### Campaign Member Import Count162- Count of contacts in `clean_contacts` (the surviving importable contacts).163164## Follow-Up Dates165166- **Lead follow-up due date**: `end_date + followup_days_after_end` (ISO YYYY-MM-DD)167- **Sponsor finance due date**: `end_date + sponsor_followup_days_after_end` (ISO YYYY-MM-DD)168- If `start_date` equals `end_date` (single-day event), use that date as the base.169170For events where only one follow-up offset is given, derive both dates from it.171172## Sorting Rules (apply everywhere)173174- Sort strings ascending alphabetically (case-sensitive, standard lexicographic).175- Sort lists of objects by the primary key specified in the template.176- Platform lists: always sort in enum order: `AUV`, `ROV`, `Underwater Camera`.177- Subject keys in campaign member actions: sort ascending by `subject_key`.178- Sponsor statuses and qualified leads: sort by `account_name`/`company_name` ascending.179- Excluded records: sort by `company_name` ascending, then `contact_name` ascending.180- Badge decisions: sort by `badge_id` ascending.181- Clean contacts: sort by `clean_contact_id` ascending.182- Duplicate keys: sort by `key` ascending.183- Removed rows: sort by `row_id` ascending.184- CRM account IDs in lists: sort ascending.185186## Data Type Conventions187188- All monetary amounts: integers (USD).189- All counts: integers.190- Boolean fields: JSON `true`/`false` (not strings).191- Nullable fields: JSON `null` when no value.192- Empty strings allowed for email/phone when no data was supplied.193- Dates: `YYYY-MM-DD` strings. Timestamps: ISO 8601 as returned by the API.194195## Key Pitfalls1961971. **Company name matching is exact**: "HelioWare Manufacturing" ≠ "HelioWare Mfg." in CRM. Use the winning row's exact company name for CRM lookups.1982. **Sponsor badge ≠ sponsor company**: A company with an active sponsor order makes ALL its badge holders sponsor attendees, even if their individual `badge_type` is `"attendee"`.1993. **Canceled ≠ inactive**: Only `order_status: "canceled"` is inactive. `proposal_sent` is an active (proposal_only) sponsor.2004. **Deferred revenue vs open balance**: `paid_deferred` means fully paid but revenue deferred for accounting — no follow-up needed. `open_invoice` means money is still owed — needs finance follow-up.2015. **Platform assignment from descriptions**: "Builds compact AUVs" → AUV; "camera modules" → Underwater Camera; "reseller" → no platform. Read descriptions literally; don't over-assign.2026. **Demo request drives ranking priority**: requested_demo=true always sorts before false, regardless of score.2037. **Suppression check happens during import cleaning**: contacts matching the suppression list by email or phone are excluded from import and counted as suppressed in totals.2048. **Empty email alone is not `missing_contact`**: a badge with a contact_name and phone but no email is still a valid lead unless the task explicitly requires email.