HarborCRM JSON Answer Procedures
Operating Workflow
- Read the user prompt and
input/payloads/answer_template.json first. Treat the template as the contract: return exactly the requested top-level keys, field names, enum spellings, nullability, and ordering.
- Use the API base URL supplied by the runner. Optionally check
/health, then fetch only the endpoints needed for the task family.
- Build normalized indexes before deciding actions:
- Email: trim, lowercase, use empty string for blanks.
- Phone: keep digits only; do not invent a country code.
- Company/account: match exact names first, then normalized names, then email domain to CRM account
domain when available.
- Contacts: match normalized email first; if no email, use account plus normalized contact name or phone.
- Derive decisions from API data, not from prose alone. Use the prompt only to choose the task family, target ID, required ranking, and any special sizing rules.
- Emit JSON only. Validate mentally or with a JSON parser when possible. Do not add explanatory text, comments, or extra fields.
API Checklist
For event handoff tasks, fetch:
/api/events/{event_id}
/api/events/{event_id}/sponsor_packages or /api/events/{event_id}/orders
/api/events/{event_id}/badges
/api/finance/invoices?event_id={event_id}
/api/crm/accounts
/api/crm/contacts
/api/crm/opportunities when the template mentions opportunities
/api/crm/campaign_members?event_id={event_id} when campaign-member actions are requested
/api/policies
For trade-show prospecting tasks, fetch:
/api/tradeshows and filter to the prompt's show_id if show metadata is needed
/api/tradeshows/{show_id}/exhibitors
/api/tradeshows/{show_id}/meeting_interest
/api/crm/accounts
/api/crm/contacts only if contact overlap is requested
/api/policies
For import-batch hygiene tasks, fetch:
/api/import_batches and filter to the prompt's batch_id
/api/import_batches/{batch_id}/raw_contacts
/api/import_batches/{batch_id}/suppression
/api/crm/accounts
/api/crm/contacts
/api/policies
Event Sponsor And Badge Handoff
Use active sponsor packages/orders plus invoices to determine sponsor status.
- Treat
confirmed sponsor packages as active sponsors.
- Treat
proposal_sent packages as active proposal sponsors.
- Treat
canceled or inactive packages as non-active. Do not count them in sponsor revenue. If a badge/account is also CRM-disqualified, prefer existing_disqualified over an inactive-sponsor reason.
paid_deferred: a confirmed sponsor whose invoice is paid/deferred or fully paid.
open_invoice: a confirmed sponsor with an open invoice or paid amount below package amount.
proposal_only: a proposal sponsor with no invoice.
not_sponsor: use only when the template explicitly asks for non-sponsor candidates.
- Use sponsor package/order amount for revenue totals. Use invoice
paid_amount only for paid/open-balance fields. Open balance is package or invoice amount minus paid amount.
- Finance follow-up accounts are
open_invoice plus proposal_only, sorted by account name. Sponsor finance due date is event end_date plus sponsor_followup_days_after_end.
Badge and lead decisions:
- Lead due date is event
end_date plus followup_days_after_end.
- Sponsor attendee means the badge company matches an active sponsor, the badge type is sponsor, or the badge contact belongs to a sponsor package. This includes proposal-only sponsors even if the badge type says attendee.
- Exclude sponsor attendees from non-sponsor lead pipeline.
- Exclude non-business badges such as student, press, academic, media, or other clearly non-commercial badge types.
- Exclude CRM accounts with
status: disqualified or a non-null disqualified_reason; use existing_disqualified.
- Use
missing_contact when a badge cannot produce a usable contact identity, normally no contact name plus no email/phone, or as directed by the template.
- Qualified non-sponsor lead accounts are business badges that are not active sponsors, not disqualified, and contactable. Use event
lead_opportunity_amount per qualified account; pipeline totals are that amount times the qualified account count unless the prompt gives a different rule.
CRM actions for event leads:
crm_account_action: update_existing when a non-disqualified CRM account is matched; otherwise create_account.
crm_contact_action: update_existing when a contact is matched by normalized email/contact identity; otherwise create_contact.
- Campaign member action is create/add when no matching member exists, update when a matching member exists with the wrong target status, and no action when it already has the target status.
- For badge-level schemas, target status is usually
attended for qualified non-sponsor badges, attended_sponsor for sponsor attendees with a badge scan, registered_sponsor for sponsor package contacts without attendance, and excluded for explicit no-import rows.
badge_only_contacts means badge contacts not already represented by a CRM contact; include sponsor contacts too if the badge creates a contact.
- Count account/contact/campaign actions from the rows the template says are importable. Do not count finance follow-up as CRM account creation/update unless the schema explicitly asks.
Common event output ordering:
- Sponsor statuses by
account_name.
- Qualified lead accounts by
account_name.
- Excluded records by
company_name, then contact_name.
- Badge decisions by
badge_id.
- Campaign-member actions by
subject_key.
- Account-name lists alphabetically.
Trade-Show Prospecting
Qualify exhibitors from descriptions and the prospecting policy.
- Include companies that manufacture, build, OEM-build, or integrate target underwater platforms.
- Target platform enums are exactly
AUV, ROV, and Underwater Camera; sort platform lists in that enum order.
- Exclude adjacent companies that do not build target platforms:
- Distributor, reseller, dealer, sales agent only:
distributor_only.
- Service, consulting, analytics-only, operator, or rented-equipment service only:
service_only.
- Sensor/probe/component vendor with no platform build: use
sensor_vendor_only or sensor_only, matching the template enum.
- University, lab, research-only, or academic group:
research_only.
- Outside the campaign's target market:
not_target_market when that enum exists.
- Preserve requested enrichment fields from exhibitors:
company_id, company_name, booth, country, website, and any template-specific fields.
- Existing CRM overlap comes from exhibitor
crm_account_id when present, or a CRM account name/domain match. Use update_existing for matched qualified exhibitors and create_account for unmatched qualified exhibitors. Excluded exhibitors usually use no_import.
Meeting interest and ranking:
- Join meeting interest by
company_name. If a qualified exhibitor has no interest row, default requested_demo to false and interest_score to 0 unless the template says otherwise.
- If the prompt gives ranking rules, follow them exactly.
- The common robotics ranking is: demo request first, interest score descending, broader platform coverage descending, company name ascending. Assign contiguous 1-based ranks after sorting.
- Use prompt-provided opportunity sizing by tier. If unspecified but priority tiers are needed, a practical default is
A for demo-requested score at least 90, B for demo-requested score at least 80, and C for other qualified leads.
- Platform coverage counts count platform appearances across qualified exhibitors, so a company with both ROV and Underwater Camera increments both.
Common trade-show output ordering:
- Unranked qualified exhibitors by
company_name.
- Ranked leads by ascending
rank.
- Excluded exhibitors or near misses by
company_name.
- CRM overlap account IDs ascending.
Import Batch Hygiene
Clean raw import rows into import-ready contacts.
- Normalize email and phone for every raw row.
- Remove unusable rows with no normalized email and no normalized phone. Use reason
missing_contact unless the template defines a narrower unusable reason.
- Remove suppressed rows when normalized email or phone matches the suppression endpoint, or when the matching CRM contact is opted out. Use reason
suppressed.
- Deduplicate the remaining rows:
- Use duplicate key
email:{normalized_email} when email exists.
- Otherwise use
phone:{normalized_phone}.
- Pick one winner per duplicate key. Prefer higher-trust sources such as
partner_upload and sponsor_form over generic forms or manual uploads; then prefer newer captured_at; then prefer richer contact data; then use row ID as a deterministic tie-breaker.
- Record removed duplicate rows with reason
duplicate.
- Build each clean contact from the winning row's original company/contact/source/timestamp fields, but normalized email and phone.
- Match CRM account by email domain, exhibitor/account ID when supplied, or normalized company name. Match CRM contact by normalized email.
- Set
crm_action to update_existing for a matched non-disqualified account, create_account for no account match, suppress for suppressed rows, and no_import for duplicates or unusable rows.
Import counts:
duplicate_removed_count is the number of duplicate rows removed, not the number of duplicate groups.
duplicate_keys include one object per duplicate group, sorted by key, with the winning row and removed rows sorted deterministically.
unusable_removed_count counts missing-contact/no-contactable rows.
suppressed_removed_count counts rows removed by suppression or opt-out.
import_action_totals.no_import includes duplicate removals plus unusable removals.
import_action_totals.suppress includes suppressed removals.
campaign_member_import_count is the number of surviving clean contacts that will enter the campaign, usually clean contacts whose action is create_account or update_existing.
Common import output ordering:
clean_contacts by clean_contact_id.
duplicate_summary.duplicate_keys by key.
removal_summary.removed_rows by row_id.
Pitfalls
- Do not use current date for follow-up deadlines; use event dates and event-specific day offsets.
- Do not turn paid amount into sponsor revenue. Revenue totals use package/order amount; open balance is separate.
- Do not qualify sponsors as sales leads, including proposal-only sponsor attendees.
- Do not treat a canceled sponsor package as active sponsorship.
- Do not add
+1 to phones that lack it; normalized phone is only the digits present in the source.
- Do not infer a contact update from an account match alone; account and contact actions are separate.
- Do not count existing disqualified CRM accounts as qualified leads even when badge or meeting interest is strong.
- Do not rename enum values to nicer wording. Match the answer template exactly.
1---2name: task-group-001-fewshot-attempt-013description: Produce HarborCRM JSON answers for evaluation tasks that provide prompts, answer_template.json files, and HarborCRM API data. Use when reconciling event sponsorships, badge leads, campaign members, finance invoices, trade-show prospecting, or import batch hygiene into exact JSON output schemas with controlled enums, counts, dates, and CRM actions.4---56# HarborCRM JSON Answer Procedures78## Operating Workflow9101. Read the user prompt and `input/payloads/answer_template.json` first. Treat the template as the contract: return exactly the requested top-level keys, field names, enum spellings, nullability, and ordering.112. Use the API base URL supplied by the runner. Optionally check `/health`, then fetch only the endpoints needed for the task family.123. Build normalized indexes before deciding actions:13 - Email: trim, lowercase, use empty string for blanks.14 - Phone: keep digits only; do not invent a country code.15 - Company/account: match exact names first, then normalized names, then email domain to CRM account `domain` when available.16 - Contacts: match normalized email first; if no email, use account plus normalized contact name or phone.174. Derive decisions from API data, not from prose alone. Use the prompt only to choose the task family, target ID, required ranking, and any special sizing rules.185. Emit JSON only. Validate mentally or with a JSON parser when possible. Do not add explanatory text, comments, or extra fields.1920## API Checklist2122For event handoff tasks, fetch:2324- `/api/events/{event_id}`25- `/api/events/{event_id}/sponsor_packages` or `/api/events/{event_id}/orders`26- `/api/events/{event_id}/badges`27- `/api/finance/invoices?event_id={event_id}`28- `/api/crm/accounts`29- `/api/crm/contacts`30- `/api/crm/opportunities` when the template mentions opportunities31- `/api/crm/campaign_members?event_id={event_id}` when campaign-member actions are requested32- `/api/policies`3334For trade-show prospecting tasks, fetch:3536- `/api/tradeshows` and filter to the prompt's `show_id` if show metadata is needed37- `/api/tradeshows/{show_id}/exhibitors`38- `/api/tradeshows/{show_id}/meeting_interest`39- `/api/crm/accounts`40- `/api/crm/contacts` only if contact overlap is requested41- `/api/policies`4243For import-batch hygiene tasks, fetch:4445- `/api/import_batches` and filter to the prompt's `batch_id`46- `/api/import_batches/{batch_id}/raw_contacts`47- `/api/import_batches/{batch_id}/suppression`48- `/api/crm/accounts`49- `/api/crm/contacts`50- `/api/policies`5152## Event Sponsor And Badge Handoff5354Use active sponsor packages/orders plus invoices to determine sponsor status.5556- Treat `confirmed` sponsor packages as active sponsors.57- Treat `proposal_sent` packages as active proposal sponsors.58- Treat `canceled` or inactive packages as non-active. Do not count them in sponsor revenue. If a badge/account is also CRM-disqualified, prefer `existing_disqualified` over an inactive-sponsor reason.59- `paid_deferred`: a confirmed sponsor whose invoice is paid/deferred or fully paid.60- `open_invoice`: a confirmed sponsor with an open invoice or paid amount below package amount.61- `proposal_only`: a proposal sponsor with no invoice.62- `not_sponsor`: use only when the template explicitly asks for non-sponsor candidates.63- Use sponsor package/order amount for revenue totals. Use invoice `paid_amount` only for paid/open-balance fields. Open balance is package or invoice amount minus paid amount.64- Finance follow-up accounts are `open_invoice` plus `proposal_only`, sorted by account name. Sponsor finance due date is event `end_date` plus `sponsor_followup_days_after_end`.6566Badge and lead decisions:6768- Lead due date is event `end_date` plus `followup_days_after_end`.69- Sponsor attendee means the badge company matches an active sponsor, the badge type is sponsor, or the badge contact belongs to a sponsor package. This includes proposal-only sponsors even if the badge type says attendee.70- Exclude sponsor attendees from non-sponsor lead pipeline.71- Exclude non-business badges such as student, press, academic, media, or other clearly non-commercial badge types.72- Exclude CRM accounts with `status: disqualified` or a non-null `disqualified_reason`; use `existing_disqualified`.73- Use `missing_contact` when a badge cannot produce a usable contact identity, normally no contact name plus no email/phone, or as directed by the template.74- Qualified non-sponsor lead accounts are business badges that are not active sponsors, not disqualified, and contactable. Use event `lead_opportunity_amount` per qualified account; pipeline totals are that amount times the qualified account count unless the prompt gives a different rule.7576CRM actions for event leads:7778- `crm_account_action`: `update_existing` when a non-disqualified CRM account is matched; otherwise `create_account`.79- `crm_contact_action`: `update_existing` when a contact is matched by normalized email/contact identity; otherwise `create_contact`.80- Campaign member action is create/add when no matching member exists, update when a matching member exists with the wrong target status, and no action when it already has the target status.81- For badge-level schemas, target status is usually `attended` for qualified non-sponsor badges, `attended_sponsor` for sponsor attendees with a badge scan, `registered_sponsor` for sponsor package contacts without attendance, and `excluded` for explicit no-import rows.82- `badge_only_contacts` means badge contacts not already represented by a CRM contact; include sponsor contacts too if the badge creates a contact.83- Count account/contact/campaign actions from the rows the template says are importable. Do not count finance follow-up as CRM account creation/update unless the schema explicitly asks.8485Common event output ordering:8687- Sponsor statuses by `account_name`.88- Qualified lead accounts by `account_name`.89- Excluded records by `company_name`, then `contact_name`.90- Badge decisions by `badge_id`.91- Campaign-member actions by `subject_key`.92- Account-name lists alphabetically.9394## Trade-Show Prospecting9596Qualify exhibitors from descriptions and the prospecting policy.9798- Include companies that manufacture, build, OEM-build, or integrate target underwater platforms.99- Target platform enums are exactly `AUV`, `ROV`, and `Underwater Camera`; sort platform lists in that enum order.100- Exclude adjacent companies that do not build target platforms:101 - Distributor, reseller, dealer, sales agent only: `distributor_only`.102 - Service, consulting, analytics-only, operator, or rented-equipment service only: `service_only`.103 - Sensor/probe/component vendor with no platform build: use `sensor_vendor_only` or `sensor_only`, matching the template enum.104 - University, lab, research-only, or academic group: `research_only`.105 - Outside the campaign's target market: `not_target_market` when that enum exists.106- Preserve requested enrichment fields from exhibitors: `company_id`, `company_name`, `booth`, `country`, `website`, and any template-specific fields.107- Existing CRM overlap comes from exhibitor `crm_account_id` when present, or a CRM account name/domain match. Use `update_existing` for matched qualified exhibitors and `create_account` for unmatched qualified exhibitors. Excluded exhibitors usually use `no_import`.108109Meeting interest and ranking:110111- Join meeting interest by `company_name`. If a qualified exhibitor has no interest row, default `requested_demo` to false and `interest_score` to 0 unless the template says otherwise.112- If the prompt gives ranking rules, follow them exactly.113- The common robotics ranking is: demo request first, interest score descending, broader platform coverage descending, company name ascending. Assign contiguous 1-based ranks after sorting.114- Use prompt-provided opportunity sizing by tier. If unspecified but priority tiers are needed, a practical default is `A` for demo-requested score at least 90, `B` for demo-requested score at least 80, and `C` for other qualified leads.115- Platform coverage counts count platform appearances across qualified exhibitors, so a company with both ROV and Underwater Camera increments both.116117Common trade-show output ordering:118119- Unranked qualified exhibitors by `company_name`.120- Ranked leads by ascending `rank`.121- Excluded exhibitors or near misses by `company_name`.122- CRM overlap account IDs ascending.123124## Import Batch Hygiene125126Clean raw import rows into import-ready contacts.1271281. Normalize email and phone for every raw row.1292. Remove unusable rows with no normalized email and no normalized phone. Use reason `missing_contact` unless the template defines a narrower unusable reason.1303. Remove suppressed rows when normalized email or phone matches the suppression endpoint, or when the matching CRM contact is opted out. Use reason `suppressed`.1314. Deduplicate the remaining rows:132 - Use duplicate key `email:{normalized_email}` when email exists.133 - Otherwise use `phone:{normalized_phone}`.134 - Pick one winner per duplicate key. Prefer higher-trust sources such as `partner_upload` and `sponsor_form` over generic forms or manual uploads; then prefer newer `captured_at`; then prefer richer contact data; then use row ID as a deterministic tie-breaker.135 - Record removed duplicate rows with reason `duplicate`.1365. Build each clean contact from the winning row's original company/contact/source/timestamp fields, but normalized email and phone.1376. Match CRM account by email domain, exhibitor/account ID when supplied, or normalized company name. Match CRM contact by normalized email.1387. Set `crm_action` to `update_existing` for a matched non-disqualified account, `create_account` for no account match, `suppress` for suppressed rows, and `no_import` for duplicates or unusable rows.139140Import counts:141142- `duplicate_removed_count` is the number of duplicate rows removed, not the number of duplicate groups.143- `duplicate_keys` include one object per duplicate group, sorted by key, with the winning row and removed rows sorted deterministically.144- `unusable_removed_count` counts missing-contact/no-contactable rows.145- `suppressed_removed_count` counts rows removed by suppression or opt-out.146- `import_action_totals.no_import` includes duplicate removals plus unusable removals.147- `import_action_totals.suppress` includes suppressed removals.148- `campaign_member_import_count` is the number of surviving clean contacts that will enter the campaign, usually clean contacts whose action is `create_account` or `update_existing`.149150Common import output ordering:151152- `clean_contacts` by `clean_contact_id`.153- `duplicate_summary.duplicate_keys` by key.154- `removal_summary.removed_rows` by `row_id`.155156## Pitfalls157158- Do not use current date for follow-up deadlines; use event dates and event-specific day offsets.159- Do not turn paid amount into sponsor revenue. Revenue totals use package/order amount; open balance is separate.160- Do not qualify sponsors as sales leads, including proposal-only sponsor attendees.161- Do not treat a canceled sponsor package as active sponsorship.162- Do not add `+1` to phones that lack it; normalized phone is only the digits present in the source.163- Do not infer a contact update from an account match alone; account and contact actions are separate.164- Do not count existing disqualified CRM accounts as qualified leads even when badge or meeting interest is strong.165- Do not rename enum values to nicer wording. Match the answer template exactly.