Overview
Company traits describe the organization a user belongs to. They’re set viaidentifyCompany() or in the companyTraits config option, stored in the companies table, and included in the AI prompt as [Company: Name].
Recommended fields
Fields marked with column are stored in dedicated database columns; all others go intocustom_fields.
| Field | Type | Storage | Description |
|---|---|---|---|
name | string | column | Company display name |
domain | string | column | Company’s website domain |
industry | string | column | Industry vertical |
plan | string | column | Subscription plan |
employee_count | number | column | Number of employees |
monthly_spend | number | custom_fields | Monthly spend or MRR |
signed_up_at | string | column | ISO 8601 date the company onboarded in your product. Optional, but strongly recommended for backfills, integration syncs, and any flow where Halo first sees the company well after the real customer relationship started. Populates the dedicated companies.signed_up_at column so it powers the “Customer since” view in the dashboard, segments, automations targeting customer tenure, broadcasts, the AI prompt, and the {{ company.signed_up_at }} email merge field. Same field name and semantics on userTraits. |
renewal_status | string | column | Renewal status. Allowed: up_for_renewal, in_progress, likely_to_renew, expansion_opportunity, set_to_cancel, set_to_cancel_trial, at_risk, renewed, lost. Auto-derived or manual. |
renewal_date | string | column | Next renewal date (ISO 8601). Auto-derived from Stripe/PandaDoc or manual. |
contract_term | string | column | Agreement frequency: monthly, quarterly, annual, bi_annual |
payment_terms | string | column | Billing frequency: monthly, quarterly, annual, bi_annual |
on_contract | boolean | column | Whether the company has an active contract |
System-managed fields
Halo sets these automatically. Do not pass them incompanyTraits or in the REST traits body. The SDK returns HTTP 400 on any of these keys.
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Halo’s internal primary key. Use company_id (your external ID) to upsert. |
created_at | string | ISO 8601 timestamp set when Halo first inserted the row. Distinct from signed_up_at (customer-set onboarding date). |
updated_at | string | ISO 8601 timestamp of the most recent write to the row. Maintained by upsert_company and a database trigger. |
health_score | number | Account health score (0 to 100). See Health Scores. |
team_size | number | Number of end users linked to this company. Computed at chat time. |
last_contacted_at | string | ISO 8601 timestamp of the last time any user at this company was contacted (broadcast, series, ticket reply, or chat). Updated automatically. |
signed_up_at vs created_at: signed_up_at is yours. Customer-set, optional, treated as a historical timestamp. Pass it to record when the company onboarded in your product. created_at is Halo’s. It tracks when the row was first inserted in our database, which is often days, months, or years later than the actual customer signup (especially for companies backfilled from Intercom, HubSpot, Stripe, or CSV import). The dashboard surfaces both as “Customer since” and “Added to Halo” respectively.Do I need to send signed_up_at?
If you call identifyCompany() synchronously the moment a company is created in your product, sending it is optional. created_at will be approximately equal to the real signup time.
You should send it whenever any of these are true:
- You’re backfilling existing companies via the API or CSV import
- You first identify a company via an integration sync (HubSpot, Stripe, Intercom). The Halo integrations auto-populate
signed_up_atfrom the upstream system precisely so dashboards and automations don’t treat every imported company as new. - You want segments and automations targeting customer tenure (“Companies that became customers in Q3 2024”, “Renewal reminder 11 months after signup”) to measure from the real start date.
How automations and segments read signed_up_at
- The series “User signs up” trigger (
entry_trigger: "user_created") fires onend_usersrow creation, not on companysigned_up_at. It’s not the right tool for “company onboarded” automations. Use aneventtrigger or asegment_matchtrigger keyed oncompany.signed_up_atinstead. - Audience filter on
company.signed_up_atreads the column directly. Companies with nosigned_up_atare skipped by that filter. There is no fallback. Sendsigned_up_atif you want segments based on company tenure to be accurate. days_since_signupin onboarding rules and Slack mode-determination preferssigned_up_at, falling back tocreated_atwhen missing. So that signal degrades gracefully.{{ company.signed_up_at }}email merge field renders blank when the column is null. Use{{ company.signed_up_at|recently }}if you mix backfilled and SDK-only companies.
Custom fields
Add any field you need:TypeScript interface
[key: string]: unknown index signature lets you pass any custom field, but the reserved keys in the System-managed fields table above (id, created_at, updated_at, health_score, team_size, last_contacted_at) are not writable. Sending any of them returns HTTP 400 from /api/sdk/companies/identify.
User-company linking
When you callidentifyCompany(), the current user is automatically linked to that company via end_users.company_id -> companies.id.
This means:
- Every chat message includes both user and company traits/context
- You only need to call
identifyCompany()once per user. The link persists - Multiple users at the same company share company-scoped data
AI prompt output
For a company with the traits above, the AI sees:snake_case keys to readable labels.
Where to go next
Context Entries
Structured state data beyond simple traits.
Examples
Real-world integration patterns.