Skip to main content

Overview

Company traits describe the organization a user belongs to. They’re set via identifyCompany() or in the companyTraits config option, stored in the companies table, and included in the AI prompt as [Company: Name]. Fields marked with column are stored in dedicated database columns; all others go into custom_fields.
Do not send created_at, updated_at, id, health_score, team_size, or last_contacted_at in companyTraits. Halo manages these. The SDK rejects requests that include any of them with HTTP 400. If you want to set the date the company onboarded in your product, use signed_up_at (above). See System-managed fields for the full list.

System-managed fields

Halo sets these automatically. Do not pass them in companyTraits or in the REST traits body. The SDK returns HTTP 400 on any of these keys.
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_at from the upstream system precisely so dashboards and outreach campaigns don’t treat every imported company as new.
  • You want segments and outreach campaigns targeting customer tenure (“Companies that became customers in Q3 2024”, “Renewal reminder 11 months after signup”) to measure from the real start date.

How outreach campaigns and segments read signed_up_at

  • The series “User signs up” trigger (entry_trigger: "user_created") fires on end_users row creation, not on company signed_up_at. It’s not the right tool for “company onboarded” campaigns. Use an event trigger or a segment_match trigger keyed on company.signed_up_at instead.
  • Audience filter on company.signed_up_at reads the column directly. Companies with no signed_up_at are skipped by that filter. There is no fallback. Send signed_up_at if you want segments based on company tenure to be accurate.
  • days_since_signup in onboarding rules and Slack mode-determination prefers signed_up_at, falling back to created_at when 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

The [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 call identifyCompany(), 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:
The formatter automatically converts snake_case keys to readable labels.

Where to go next

Context Entries

Structured state data beyond simple traits.

Examples

Real-world integration patterns.