HarborCRM Input/Output Skill
Core Workflow
- Read the user prompt and
input/payloads/answer_template.json first.
- Use the API base URL supplied by the runner or prompt. Fetch only the records needed for the requested
event_id, show_id, or batch_id, plus CRM and policy lookup data.
- Return exactly one JSON object matching the template: no prose, no extra keys, no missing required keys, exact enum spelling, integer counts/USD, and required sort order.
- Validate the final object with
jq or an equivalent JSON parser before answering.
Useful API paths:
- Policies:
GET /api/policies
- Event work:
GET /api/events/{event_id}, /orders, /badges, /sponsor_packages, /api/finance/invoices?event_id={event_id}, /api/crm/campaign_members?event_id={event_id}
- CRM lookups:
GET /api/crm/accounts, /api/crm/contacts, /api/crm/opportunities
- Tradeshow work:
GET /api/tradeshows, /api/tradeshows/{show_id}/exhibitors, /api/tradeshows/{show_id}/meeting_interest
- Import batches:
GET /api/import_batches, /api/import_batches/{batch_id}/raw_contacts, /api/import_batches/{batch_id}/suppression
Normalization
- Normalize emails by trimming whitespace and lowercasing. Use
"" when no email is supplied and the template expects a string.
- Normalize phones to digits only. Use
"" when no phone is supplied and the template expects a string.
- Match accounts by explicit
account_id/crm_account_id first, then normalized company name, then email/website domain when names vary.
- Treat CRM accounts with
status: "disqualified" or a disqualified_reason as excluded; do not update or import them as qualified leads.
- Match contacts by normalized email first, then phone/name under the matched account. An existing account does not imply an existing contact.
- Compute event due dates as
event.end_date + followup_days_after_end or event.end_date + sponsor_followup_days_after_end, formatted YYYY-MM-DD.
Event Sponsor And Lead Reconciliation
Use event details, sponsor orders/packages, invoices, badges, CRM records, and campaign members together.
Sponsor statuses:
- Treat
confirmed and proposal_sent sponsor orders as active. Exclude canceled or inactive orders from active sponsor status lists.
paid_deferred: active sponsor with a paid/deferred invoice or fully paid invoice. Use the order/package amount as revenue, include invoice fields when the template asks.
open_invoice: active sponsor with an open invoice or unpaid balance. Use full order/package amount for sponsor revenue totals and separately calculate open_balance = invoice.amount - invoice.paid_amount.
proposal_only: active proposal sponsor without an invoice. Use the order/package amount, invoice_id: null, and zero paid/open invoice balance unless the template asks for unpaid proposal total.
- Finance follow-up targets are
open_invoice plus proposal_only sponsors. Count one task per account. If an unpaid total is requested, use open invoice balances plus proposal amounts.
sponsor_packages can mirror orders; do not double count if both endpoints return the same records.
Badge and lead decisions:
- Active sponsor contacts include sponsor badge scans, active sponsor order
ticket_contacts, badges whose company matches an active sponsor account, and existing sponsor campaign members.
- Exclude active sponsor contacts from non-sponsor leads with
sponsor_attendee, but still create/update contact or campaign-member work for them when the template asks for badge-level handling.
- Exclude non-business badge types such as
student and press with non_business_badge.
- Exclude matched CRM disqualified accounts with
existing_disqualified; this takes precedence over inactive/canceled sponsor history.
- Use
inactive_sponsor_record only when a record is tied to an inactive/canceled sponsor and no stronger exclusion applies.
- Qualified non-sponsor leads are business attendees that are not active sponsors, not disqualified, and have usable contact/company facts. Use the event
lead_opportunity_amount for each qualified account.
- Account action is
update_existing when a non-disqualified account exists; otherwise create_account. Contact action is update_existing only when the person already exists; otherwise create_contact.
Campaign-member handling:
- Existing campaign member at the desired status is
no_action.
- Sponsor target statuses are usually
attended_sponsor for sponsor badge attendance and registered_sponsor for sponsor records without attendance.
- Qualified non-sponsor badge attendance usually targets
attended.
- If both
account_id and contact_id are known, use subject keys like account_id:contact_id when requested. If a contact must be created from a badge, use badge:{badge_id}.
- Badge-level exclusion counts count badge decisions, not every sponsor order contact, unless the template defines them differently.
Common event output fields:
sponsor_statuses: active sponsor accounts sorted by account_name; amounts are integer USD.
sponsor_revenue_totals: sum sponsor package/order amounts by controlled status; keep open invoice balance separate.
qualified_lead_accounts: one row per qualified non-sponsor account, sorted as the template requires.
lead_pipeline_total or open_opportunity_total_usd: number of qualified non-sponsor accounts times the event lead opportunity amount unless the prompt provides another sizing rule.
excluded_records/badge_decisions: use controlled reasons only.
crm_action_counts: count the CRM work implied by imported lead/contact/campaign-member decisions, not excluded no-op records.
Tradeshow Prospecting
Use show metadata, exhibitors, meeting interest, CRM accounts, and policies.
Qualification:
- Qualify exhibitors that build, manufacture, design, or OEM-integrate target platforms from the policy enum:
AUV, ROV, Underwater Camera.
- Exclude adjacent companies that do not build target platforms:
- distributor/dealer/reseller only:
distributor_only
- consulting, service, operations, analytics, or rented-equipment service only:
service_only
- sensor/probe-only vendor: use the template enum, commonly
sensor_vendor_only or sensor_only
- research/lab only:
research_only
- unrelated market:
not_target_market when allowed
- Sort each lead's
platforms in policy enum order: AUV, ROV, Underwater Camera.
Interest, priority, and sizing:
- Join meeting interest by company name. If no record exists, use
requested_demo: false and a zero/blank score only if the template needs those fields.
- Use prompt-specified priority and opportunity rules whenever provided.
- Learned default when no other rule is supplied: demo requested with score >= 90 is tier
A; demo requested with score >= 80 is tier B; all other qualified leads are tier C.
- When tier opportunity amounts are supplied, assign per tier and sum integer USD.
CRM and counts:
- Use exhibitor
crm_account_id or CRM account matching for overlap. Existing non-disqualified accounts get update_existing; new qualified exhibitors get create_account; excluded exhibitors get no_import.
platform_counts count each qualified exhibitor once per covered platform, so a multi-platform exhibitor increments multiple platform counters.
- Include all required count buckets, even when the value is zero.
- If the prompt asks for ranking, sort demo requests first, then interest score descending, then broader platform coverage, then company name ascending, and assign contiguous 1-based ranks. Otherwise follow the template's sort rule, often company name ascending.
Import Batch Cleanup
Use batch metadata, raw contacts, suppression records, CRM accounts, CRM contacts, and policies.
Cleaning order:
- Normalize email and phone for every raw row.
- Remove unusable rows with neither normalized email nor normalized phone as
missing_contact.
- Remove suppressed rows when normalized email or phone matches the suppression endpoint, or when a matched CRM contact is opted out. Count these as
suppress.
- Deduplicate remaining contactable rows.
- Produce clean contacts only for dedupe winners that survive suppression.
Dedupe:
- Prefer duplicate key
email:{normalized_email} when email exists; otherwise use phone:{normalized_phone}.
- Choose the winning row by higher-confidence source, then newest
captured_at, then deterministic row_id. Source confidence learned from examples: sponsor_form/partner_upload/exhibitor_form outrank webinar_form/badge_scan, which outrank manual_upload.
clean_contact_id and source_row_id should both be the winning row id unless the template instructs otherwise.
- Keep the winning row's company name, contact name, source, and timestamp; do not silently replace display values with CRM canonical names.
Import actions and summaries:
- Clean winner with existing non-disqualified account:
update_existing.
- Clean winner without an existing account:
create_account.
- Duplicate removals and missing-contact removals count as
no_import.
- Suppressed removals count as
suppress.
import_action_totals counts all raw rows by final action, not just clean contacts.
campaign_member_import_count counts surviving clean contacts that will be imported as campaign members.
- Sort clean contacts, duplicate keys, and removed rows exactly as the template specifies.
Pitfalls
- Follow the answer template over habit: field names vary (
sponsor_status vs status, add_campaign_member vs create, sensor_vendor_only vs sensor_only).
- Use full sponsor package/order amount for sponsor revenue by status; keep unpaid balances in separate fields.
- Proposal-only sponsors have no invoice but still require sponsor finance follow-up.
- Canceled sponsor orders are not active sponsors.
- Active sponsor order contacts may need to appear in exclusions even if they did not scan a badge.
- Existing account overlap does not mean existing contact overlap.
- Do not import or update disqualified CRM accounts as qualified leads.
- Do not let a high meeting-interest score qualify a distributor, service provider, sensor-only vendor, or research-only exhibitor.
- Sort at the end after all inclusion/exclusion decisions are final.
1---2name: task-group-001-fewshot-attempt-033description: HarborCRM JSON-output procedures for event sponsor/lead reconciliation, tradeshow prospecting, and CRM import-batch cleanup using HarborCRM API data and answer templates. Use when a task asks Codex to produce structured JSON from HarborCRM events, sponsor packages/orders, badges, invoices, CRM accounts/contacts/opportunities, campaign members, tradeshow exhibitors/meeting interest, import batches/raw contacts/suppression, or policy metadata.4---56# HarborCRM Input/Output Skill78## Core Workflow9101. Read the user prompt and `input/payloads/answer_template.json` first.112. Use the API base URL supplied by the runner or prompt. Fetch only the records needed for the requested `event_id`, `show_id`, or `batch_id`, plus CRM and policy lookup data.123. Return exactly one JSON object matching the template: no prose, no extra keys, no missing required keys, exact enum spelling, integer counts/USD, and required sort order.134. Validate the final object with `jq` or an equivalent JSON parser before answering.1415Useful API paths:1617- Policies: `GET /api/policies`18- Event work: `GET /api/events/{event_id}`, `/orders`, `/badges`, `/sponsor_packages`, `/api/finance/invoices?event_id={event_id}`, `/api/crm/campaign_members?event_id={event_id}`19- CRM lookups: `GET /api/crm/accounts`, `/api/crm/contacts`, `/api/crm/opportunities`20- Tradeshow work: `GET /api/tradeshows`, `/api/tradeshows/{show_id}/exhibitors`, `/api/tradeshows/{show_id}/meeting_interest`21- Import batches: `GET /api/import_batches`, `/api/import_batches/{batch_id}/raw_contacts`, `/api/import_batches/{batch_id}/suppression`2223## Normalization2425- Normalize emails by trimming whitespace and lowercasing. Use `""` when no email is supplied and the template expects a string.26- Normalize phones to digits only. Use `""` when no phone is supplied and the template expects a string.27- Match accounts by explicit `account_id`/`crm_account_id` first, then normalized company name, then email/website domain when names vary.28- Treat CRM accounts with `status: "disqualified"` or a `disqualified_reason` as excluded; do not update or import them as qualified leads.29- Match contacts by normalized email first, then phone/name under the matched account. An existing account does not imply an existing contact.30- Compute event due dates as `event.end_date + followup_days_after_end` or `event.end_date + sponsor_followup_days_after_end`, formatted `YYYY-MM-DD`.3132## Event Sponsor And Lead Reconciliation3334Use event details, sponsor orders/packages, invoices, badges, CRM records, and campaign members together.3536Sponsor statuses:3738- Treat `confirmed` and `proposal_sent` sponsor orders as active. Exclude `canceled` or inactive orders from active sponsor status lists.39- `paid_deferred`: active sponsor with a paid/deferred invoice or fully paid invoice. Use the order/package amount as revenue, include invoice fields when the template asks.40- `open_invoice`: active sponsor with an open invoice or unpaid balance. Use full order/package amount for sponsor revenue totals and separately calculate `open_balance = invoice.amount - invoice.paid_amount`.41- `proposal_only`: active proposal sponsor without an invoice. Use the order/package amount, `invoice_id: null`, and zero paid/open invoice balance unless the template asks for unpaid proposal total.42- Finance follow-up targets are `open_invoice` plus `proposal_only` sponsors. Count one task per account. If an unpaid total is requested, use open invoice balances plus proposal amounts.43- `sponsor_packages` can mirror `orders`; do not double count if both endpoints return the same records.4445Badge and lead decisions:4647- Active sponsor contacts include sponsor badge scans, active sponsor order `ticket_contacts`, badges whose company matches an active sponsor account, and existing sponsor campaign members.48- Exclude active sponsor contacts from non-sponsor leads with `sponsor_attendee`, but still create/update contact or campaign-member work for them when the template asks for badge-level handling.49- Exclude non-business badge types such as `student` and `press` with `non_business_badge`.50- Exclude matched CRM disqualified accounts with `existing_disqualified`; this takes precedence over inactive/canceled sponsor history.51- Use `inactive_sponsor_record` only when a record is tied to an inactive/canceled sponsor and no stronger exclusion applies.52- Qualified non-sponsor leads are business attendees that are not active sponsors, not disqualified, and have usable contact/company facts. Use the event `lead_opportunity_amount` for each qualified account.53- Account action is `update_existing` when a non-disqualified account exists; otherwise `create_account`. Contact action is `update_existing` only when the person already exists; otherwise `create_contact`.5455Campaign-member handling:5657- Existing campaign member at the desired status is `no_action`.58- Sponsor target statuses are usually `attended_sponsor` for sponsor badge attendance and `registered_sponsor` for sponsor records without attendance.59- Qualified non-sponsor badge attendance usually targets `attended`.60- If both `account_id` and `contact_id` are known, use subject keys like `account_id:contact_id` when requested. If a contact must be created from a badge, use `badge:{badge_id}`.61- Badge-level exclusion counts count badge decisions, not every sponsor order contact, unless the template defines them differently.6263Common event output fields:6465- `sponsor_statuses`: active sponsor accounts sorted by `account_name`; amounts are integer USD.66- `sponsor_revenue_totals`: sum sponsor package/order amounts by controlled status; keep open invoice balance separate.67- `qualified_lead_accounts`: one row per qualified non-sponsor account, sorted as the template requires.68- `lead_pipeline_total` or `open_opportunity_total_usd`: number of qualified non-sponsor accounts times the event lead opportunity amount unless the prompt provides another sizing rule.69- `excluded_records`/`badge_decisions`: use controlled reasons only.70- `crm_action_counts`: count the CRM work implied by imported lead/contact/campaign-member decisions, not excluded no-op records.7172## Tradeshow Prospecting7374Use show metadata, exhibitors, meeting interest, CRM accounts, and policies.7576Qualification:7778- Qualify exhibitors that build, manufacture, design, or OEM-integrate target platforms from the policy enum: `AUV`, `ROV`, `Underwater Camera`.79- Exclude adjacent companies that do not build target platforms:80 - distributor/dealer/reseller only: `distributor_only`81 - consulting, service, operations, analytics, or rented-equipment service only: `service_only`82 - sensor/probe-only vendor: use the template enum, commonly `sensor_vendor_only` or `sensor_only`83 - research/lab only: `research_only`84 - unrelated market: `not_target_market` when allowed85- Sort each lead's `platforms` in policy enum order: `AUV`, `ROV`, `Underwater Camera`.8687Interest, priority, and sizing:8889- Join meeting interest by company name. If no record exists, use `requested_demo: false` and a zero/blank score only if the template needs those fields.90- Use prompt-specified priority and opportunity rules whenever provided.91- Learned default when no other rule is supplied: demo requested with score >= 90 is tier `A`; demo requested with score >= 80 is tier `B`; all other qualified leads are tier `C`.92- When tier opportunity amounts are supplied, assign per tier and sum integer USD.9394CRM and counts:9596- Use exhibitor `crm_account_id` or CRM account matching for overlap. Existing non-disqualified accounts get `update_existing`; new qualified exhibitors get `create_account`; excluded exhibitors get `no_import`.97- `platform_counts` count each qualified exhibitor once per covered platform, so a multi-platform exhibitor increments multiple platform counters.98- Include all required count buckets, even when the value is zero.99- If the prompt asks for ranking, sort demo requests first, then interest score descending, then broader platform coverage, then company name ascending, and assign contiguous 1-based ranks. Otherwise follow the template's sort rule, often company name ascending.100101## Import Batch Cleanup102103Use batch metadata, raw contacts, suppression records, CRM accounts, CRM contacts, and policies.104105Cleaning order:1061071. Normalize email and phone for every raw row.1082. Remove unusable rows with neither normalized email nor normalized phone as `missing_contact`.1093. Remove suppressed rows when normalized email or phone matches the suppression endpoint, or when a matched CRM contact is opted out. Count these as `suppress`.1104. Deduplicate remaining contactable rows.1115. Produce clean contacts only for dedupe winners that survive suppression.112113Dedupe:114115- Prefer duplicate key `email:{normalized_email}` when email exists; otherwise use `phone:{normalized_phone}`.116- Choose the winning row by higher-confidence source, then newest `captured_at`, then deterministic `row_id`. Source confidence learned from examples: `sponsor_form`/`partner_upload`/`exhibitor_form` outrank `webinar_form`/`badge_scan`, which outrank `manual_upload`.117- `clean_contact_id` and `source_row_id` should both be the winning row id unless the template instructs otherwise.118- Keep the winning row's company name, contact name, source, and timestamp; do not silently replace display values with CRM canonical names.119120Import actions and summaries:121122- Clean winner with existing non-disqualified account: `update_existing`.123- Clean winner without an existing account: `create_account`.124- Duplicate removals and missing-contact removals count as `no_import`.125- Suppressed removals count as `suppress`.126- `import_action_totals` counts all raw rows by final action, not just clean contacts.127- `campaign_member_import_count` counts surviving clean contacts that will be imported as campaign members.128- Sort clean contacts, duplicate keys, and removed rows exactly as the template specifies.129130## Pitfalls131132- Follow the answer template over habit: field names vary (`sponsor_status` vs `status`, `add_campaign_member` vs `create`, `sensor_vendor_only` vs `sensor_only`).133- Use full sponsor package/order amount for sponsor revenue by status; keep unpaid balances in separate fields.134- Proposal-only sponsors have no invoice but still require sponsor finance follow-up.135- Canceled sponsor orders are not active sponsors.136- Active sponsor order contacts may need to appear in exclusions even if they did not scan a badge.137- Existing account overlap does not mean existing contact overlap.138- Do not import or update disqualified CRM accounts as qualified leads.139- Do not let a high meeting-interest score qualify a distributor, service provider, sensor-only vendor, or research-only exhibitor.140- Sort at the end after all inclusion/exclusion decisions are final.