Skip to main content

Overview

The most effective HaloAgents integrations go beyond basic user identity. When the AI agent has deep context about a user’s product usage, billing status, and current issues, it can resolve support questions instantly instead of asking clarifying questions. This guide walks you through the thinking process of planning what data to send, how to structure it, and how to keep it up to date — including for users who already exist in your system.
Before reading this guide, make sure you’re familiar with the Data Model and the difference between traits and context.

Step 1: Map Out Your Product Data

Start by listing the key data in your app that would help an AI support agent answer questions. Think about what a human support agent would look up before responding to a ticket. For a forms & scheduling SaaS product, the list might look like:
DataWhy it matters
Billing plan & status”Can I use feature X?” depends on their plan
Forms createdHelps answer “How do I edit my form?” or “Where are my forms?”
Form submissions receivedIndicates onboarding progress, troubleshooting submission issues
Scheduling pages & meetingsContext for scheduling-related questions
Connected integrations”Why isn’t my HubSpot sync working?” requires knowing connection status
AI credit usage”How many AI credits do I have left?”
Onboarding milestonesDetermines if the user is new vs. experienced
You don’t need to send every piece of data in your database. Focus on data the AI would need to answer the most common support questions.

Step 2: Decide What Goes Where

HaloAgents has three data mechanisms, each with a different purpose:
MechanismBest forExample
User traits (identify)Simple attributes about the personname, email, role, plan
Company traits (identifyCompany)Simple attributes about the organizationcompany name, plan, industry
Context entries (setContext)Rich, structured data the AI should referenceintegration statuses, metrics, feature flags
The rule of thumb: if it’s a single value (string, number, boolean), it’s probably a trait. If it’s structured data (lists, nested objects, multiple related values), it’s context.

User-Scoped vs. Company-Scoped

Data can be scoped to the individual user or to their company/team:
ScopeWhen to useExamples
User-scopedData specific to one personHas this user created a form? Has this user connected an integration?
Company-scopedData shared across the teamTotal forms across the team, billing plan, connected integrations
Most product data is company-scoped (plans, integrations, aggregate counts). User-scoped data is typically about the individual’s activity or onboarding progress.

Step 3: Design Your Traits

User Traits

Start with the recommended fields (name, email, role), then add product-specific traits that describe the user’s state:
ha.identify(userId, {
  // Standard identity
  name: "Jane Doe",
  email: "[email protected]",
  role: "admin",

  // Product-specific
  plan: "pro",
  has_created_form: true,
  has_received_submission: true,
  has_booked_meeting: false,
  has_connected_integration: true,
});
Notice the has_* boolean fields — these are onboarding milestones. The AI can use them to determine if the user is new (“I see you haven’t created a form yet — let me walk you through it”) vs. experienced.

Company Traits

Start with recommended fields (name, plan, industry), then add billing and plan-level data:
ha.identifyCompany(teamId, {
  // Standard identity
  name: "Acme Corp",
  plan: "business",
  industry: "SaaS",

  // Billing details
  billing_status: "active",
  billing_interval: "year",
  stripe_customer_id: "cus_xxx",

  // Aggregate counts
  total_forms: 12,
  total_submissions: 1847,
  total_scheduling_pages: 3,
  total_meetings: 89,

  // Usage
  ai_credits_used: 45,
  ai_credits_included: 1200,
});

Step 4: Design Your Context Entries

Context entries give the AI structured data it can reference in detail. Plan context entries around the types of questions users ask.

Onboarding & Activity (Status)

Track what the user has and hasn’t done. This helps the AI provide guidance appropriate to their experience level.
ha.setContext("user_activity", {
  label: "User Product Activity",
  type: "status",
  value: {
    has_created_form: { status: true },
    has_received_submission: { status: true },
    has_booked_meeting: { status: false },
    has_connected_integration: { status: true },
  },
});
Now when a user asks “How do I get started?”, the AI knows they’ve already created a form and received submissions, so it can skip those steps and suggest booking a meeting or exploring integrations.

Product Metrics (Metric)

Aggregate usage data helps the AI answer “how much” and “how many” questions.
ha.setContext("product_usage", {
  label: "Product Usage Analytics",
  type: "metric",
  value: {
    total_forms: 12,
    total_submissions: 1847,
    total_scheduling_pages: 3,
    total_meetings: 89,
  },
});

Integration Status (Integration)

If your product has integrations, send their connection states. This is one of the highest-value context types — a huge portion of support questions are about integrations.
ha.setContext("integrations", {
  label: "Connected Integrations",
  type: "integration",
  value: {
    hubspot: { connected: true, status: "active" },
    slack: { connected: true, status: "active" },
    google_sheets: { connected: true, status: "active" },
    zapier: { connected: false },
  },
});
When a user asks “Why isn’t my Zapier integration working?”, the AI immediately knows Zapier isn’t connected and can guide them through setup — no back-and-forth needed.

Billing & Usage (Metric)

Billing context helps the AI answer plan and pricing questions without escalation.
ha.setContext("billing", {
  label: "Billing & Usage",
  type: "metric",
  value: {
    plan: "business",
    billing_status: "active",
    billing_interval: "year",
    ai_credits_used: 45,
    ai_credits_included: 1200,
  },
});

Step 5: Plan Your Data Sources

Now that you know what to send, decide where the data comes from in your app.

Parallel Queries for Performance

Fetch all your data in parallel. If you’re using a database, most of these can be lightweight count queries:
// Example: fetching all data in parallel from your database
const [
  subscription,
  formsCount,
  submissionsCount,
  schedulingCount,
  meetingsCount,
  integrations,
] = await Promise.all([
  db.subscriptions.findOne({ teamId }),
  db.forms.count({ teamId }),
  db.submissions.count({ teamId }),
  db.schedulingPages.count({ teamId }),
  db.meetings.count({ teamId }),
  db.integrations.findMany({ teamId, isActive: true }),
]);
Use count-only queries wherever possible. You don’t need to fetch full rows to get aggregate numbers — just the counts.

Where to Run the Queries

The best place is typically your app’s authenticated layout or dashboard shell — the component that wraps every authenticated page. This ensures:
  • Data is fetched once per session (not on every page navigation)
  • The user is already authenticated, so you have their ID and team ID
  • The data is available before the user opens the chat widget
App Shell / Dashboard Layout
  ├── Fetch user & company data (server-side)
  ├── Pass as props to client
  └── Client component
       ├── ha.identify(userId, traits)
       ├── ha.identifyCompany(teamId, traits)
       ├── ha.setContext("user_activity", {...})
       ├── ha.setContext("product_usage", {...})
       ├── ha.setContext("integrations", {...})
       └── ha.setContext("billing", {...})

Step 6: Plan Your Context Refresh Strategy

Data goes stale. A user might connect an integration, receive a new submission, or change their plan — and the AI should know about it. Plan multiple layers to keep context fresh.

Layer 1: On Dashboard Load (Client-Side Script)

This is your primary mechanism. Every time the user loads your app, HaloAgents sends fresh data. This covers all users, including existing users who were active before you integrated HaloAgents. The first time they log in after your deployment, all their historical data (form counts, integrations, billing) is fetched and sent.
// Runs on every dashboard load
useEffect(() => {
  const ha = HaloAgents.init({ orgId, apiKey, userId });

  ha.identify(userId, userTraits);
  ha.identifyCompany(teamId, companyTraits);
  ha.setContext("product_usage", productUsageContext);
  ha.setContext("integrations", integrationsContext);
  ha.setContext("billing", billingContext);

  return () => ha.destroy();
}, []);

Layer 2: Event-Driven Updates (Server-Side REST API)

For critical state changes that happen between page loads, update HaloAgents immediately using the REST API. Common triggers:
EventWhat to update
User or team created at signupUser + company traits via /api/sdk/users/identify and /api/sdk/companies/identify (required when signup and first widget load are separate)
Integration connected/disconnectedIntegration context
Plan upgraded/downgradedBilling traits & context
Key milestones (first form created, etc.)User activity context
Use a shared helper function to avoid duplicating logic:
// lib/haloagents.ts
export async function syncCompanyContext(teamId: string) {
  const [integrations, subscription, counts] = await Promise.all([
    db.integrations.findMany({ teamId }),
    db.subscriptions.findOne({ teamId }),
    fetchAggregateCountsForTeam(teamId),
  ]);

  await fetch("https://api.haloagents.ai/api/sdk/companies/identify", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.HALOAGENTS_API_KEY}`,
    },
    body: JSON.stringify({
      company_id: teamId,
      traits: {
        name: subscription.teamName,
        plan: subscription.plan || "free",
      },
      context: {
        product_usage: {
          label: "Product Usage Analytics",
          type: "metric",
          value: counts,
        },
        integrations: {
          label: "Connected Integrations",
          type: "integration",
          value: formatIntegrations(integrations),
        },
      },
    }),
  });
}
Call this helper from your event handlers as fire-and-forget (wrapped in try/catch so it never breaks the main flow):
// In your OAuth callback or webhook handler
try {
  await syncCompanyContext(teamId);
} catch (err) {
  console.error("HaloAgents sync failed:", err);
  // Don't throw -- this should never break the main flow
}

Layer 3: Periodic Background Sync

For aggregate data that changes gradually (submission counts, meeting counts, credit usage), schedule a periodic background job. Weekly is usually sufficient.
// Example: weekly cron job (pseudo-code)
async function weeklyContextSync() {
  const teams = await db.teams.findAll({ isActive: true });

  for (const team of teams) {
    await syncCompanyContext(team.id);
    // Add a small delay or use batching to avoid rate limits
  }
}
This catches any changes that happened outside of the event-driven updates and ensures data stays fresh even if a user hasn’t logged in recently.

Layer 4: One-Time Backfill for Existing Users

When you first integrate HaloAgents, your existing users already have data (forms, submissions, integrations) that the AI should know about. Run a one-time backfill to populate HaloAgents with historical data for all existing users and companies.
// One-time backfill script
async function backfillAllTeams() {
  const teams = await db.teams.findAll();
  let synced = 0;

  for (const team of teams) {
    try {
      await syncCompanyContext(team.id);
      synced++;
    } catch (err) {
      console.error(`Failed to sync team ${team.id}:`, err);
      // Continue with other teams
    }
  }

  console.log(`Backfill complete: ${synced}/${teams.length} teams synced`);
}
Reuse the same syncCompanyContext() helper for the backfill, periodic sync, and event-driven updates. This ensures every team gets the same data structure regardless of how the sync is triggered.

Step 7: Review Your Implementation Checklist

Before shipping, verify you’ve covered each area:
1

User traits are set on login and at signup

identify() is called with name, email, role, and product-specific fields on every authenticated load. If signup and first login can be separate, the signup API also calls /api/sdk/users/identify with signed_up_at from your product database.
2

Company traits are set on login

identifyCompany() is called with company name, plan, and billing details.
3

Context entries cover key product areas

At minimum: product usage metrics, integration statuses, and billing/usage data.
4

Dashboard load sends fresh data

Every time the user opens the app, the latest data is sent via the script.
5

Critical events trigger server-side syncs

Integration changes, plan changes, and key milestones push updates via the REST API.
6

A periodic job refreshes aggregate data

A weekly (or daily) background job updates counts and usage metrics.
7

Existing users are backfilled

A one-time script populates HaloAgents with historical data for all existing users.

Putting It All Together

Here’s a complete example for a forms & scheduling SaaS app, showing how all the pieces connect:
// 1. Server-side: Fetch data in your dashboard layout
const [user, team, subscription, formsCount, submissionsCount,
       schedulingCount, meetingsCount, integrations] = await Promise.all([
  getUser(userId),
  getTeam(teamId),
  getSubscription(teamId),
  db.forms.count({ teamId }),
  db.submissions.count({ teamId }),
  db.schedulingPages.count({ teamId }),
  db.meetings.count({ teamId }),
  db.integrations.findMany({ teamId }),
]);

// Also fire a server-side sync (fire-and-forget)
syncCompanyContext(teamId).catch(console.error);

// 2. Client-side: Pass data to HaloAgents
ha.identify(userId, {
  name: user.name,
  email: user.email,
  role: user.role,
  plan: subscription?.plan || "free",
  has_created_form: formsCount > 0,
  has_received_submission: submissionsCount > 0,
  has_booked_meeting: meetingsCount > 0,
  has_connected_integration: integrations.some(i => i.isActive),
});

ha.identifyCompany(teamId, {
  name: team.name,
  plan: subscription?.plan || "free",
  billing_status: subscription?.status || "none",
  billing_interval: subscription?.interval,
  total_forms: formsCount,
  total_submissions: submissionsCount,
  total_scheduling_pages: schedulingCount,
  total_meetings: meetingsCount,
  ai_credits_used: team.aiCreditsUsed,
  ai_credits_included: team.aiCreditsIncluded,
});

// 3. Set structured context
ha.setContext("user_activity", {
  label: "User Product Activity",
  type: "status",
  value: {
    has_created_form: { status: formsCount > 0 },
    has_received_submission: { status: submissionsCount > 0 },
    has_booked_meeting: { status: meetingsCount > 0 },
    has_connected_integration: { status: integrations.some(i => i.isActive) },
  },
});

ha.setContext("product_usage", {
  label: "Product Usage Analytics",
  type: "metric",
  value: {
    total_forms: formsCount,
    total_submissions: submissionsCount,
    total_scheduling_pages: schedulingCount,
    total_meetings: meetingsCount,
  },
});

ha.setContext("integrations", {
  label: "Connected Integrations",
  type: "integration",
  value: Object.fromEntries(
    integrations.map(i => [
      i.type,
      {
        connected: i.isActive,
        status: i.lastError ? "error" : i.isActive ? "active" : "disconnected",
        ...(i.lastError ? { error: i.lastError } : {}),
      }
    ])
  ),
});

ha.setContext("billing", {
  label: "Billing & Usage",
  type: "metric",
  value: {
    plan: subscription?.plan || "free",
    billing_status: subscription?.status || "none",
    billing_interval: subscription?.interval,
    ai_credits_used: team.aiCreditsUsed,
    ai_credits_included: team.aiCreditsIncluded,
  },
});
With this setup, the AI can handle questions like:
  • “How many submissions have I received?” — 1,847 total across 12 forms.
  • “Why isn’t HubSpot syncing?” — It sees the error status and can guide troubleshooting.
  • “Can I use the AI features?” — It knows the plan and remaining credits.
  • “How do I get started?” — It knows the user has already created forms but hasn’t booked a meeting, so it suggests that next.
  • “What plan am I on?” — Business plan, annual billing, active status.

Performance Tips

When you only need totals (form count, submission count), use your database’s count mechanism instead of fetching and counting rows. This is significantly faster and transfers less data.
Use Promise.all() (or your language’s equivalent) to run independent database queries simultaneously. This turns 7 sequential queries into 1 parallel batch.
When calling the REST API from event handlers (OAuth callbacks, webhooks), wrap the call in try/catch and don’t await it in the critical path. A failed sync should never break your user’s flow.
When backfilling all existing teams, add delays between API calls or use a job queue with concurrency limits to avoid overwhelming your database or the HaloAgents API.
For integration queries, select only the columns you need (type, is_active, last_error) instead of full rows. For billing, a single-row lookup is sufficient.