---
title: Track product usage
description: A nightly job that counts each user's PostHog login events and writes the 7-day total onto their Attio person record.
slug: posthog-logins-to-attio
group: gtm
trigger: Daily cron at 6:00 AM ET
steps:
  - "attio: page through every user record"
  - "posthog: count logged_in events per user, last 7 days"
  - "attio: write logged_in_7d to the linked person"
  - "log: report the sync total"
integrations:
  - posthog
  - attio
outcome: See users metrics in your CRM
publishedAt: "2026-07-16"
updatedAt: "2026-07-16"
durable: true
---

Sync your event data from Posthog to Attio and know how your users have been using your product.

## Prerequisites

1. A Terse project ( `terse init` )
2. Connected Posthog and Attio integrations. Run terse integrate and follow steps to connect each one.

## What this does

Syncs login event counts once a day from Posthog to Attio per user.

## The job

```typescript
import { createJob } from "terse-sdk";
import type { AttioRecordFor } from "../terse.generated";
import {
  AttioObject,
  PosthogProject,
  toolbox,
  Triggers,
} from "../terse.generated";

const POSTHOG_PROJECT: PosthogProject = PosthogProject.Terse;

const ATTIO_PAGE_SIZE = 100;
const USER_CONCURRENCY = 5;

createJob({
  name: "Sync PostHog CLI logins to Attio people",

  triggers: [
    Triggers.schedule.cron({
      expression: "0 6 * * *",
      timezone: "America/New_York",
    }),
  ],

  durable: true,

  onTrigger: async () => {
    const users = await fetchSyncableUsers();
    console.log(`Syncing ${users.length} Attio user(s)`);

    await chunk(users, USER_CONCURRENCY).reduce(
      async (previousBatches, batch) => {
        await previousBatches;
        await Promise.all(batch.map(syncUser));
      },
      Promise.resolve(),
    );

    console.log(`Updated ${users.length} person record(s)`);
  },
});

async function syncUser(user: SyncableUser): Promise<void> {
  // PostHog aggregates server-side: one call returns per-event counts for this user.
  const counts = await toolbox.posthog.listEventNames({
    projectId: POSTHOG_PROJECT.projectId,
    distinctId: user.userId,
    dateFrom: "-7d",
  });
  const loggedIn7d =
    counts.eventCounts.find((entry) => entry.eventName === "logged_in")
      ?.count ?? 0;

  await toolbox.attio.updateRecord({
    object: AttioObject.Person,
    recordId: user.personRecordId,
    values: { logged_in_7d: loggedIn7d },
  });
}

async function fetchSyncableUsers(offset = 0): Promise<SyncableUser[]> {
  const page = await toolbox.attio.queryRecords({
    object: AttioObject.User,
    limit: ATTIO_PAGE_SIZE,
    offset,
  });
  const syncable = page.records
    .map(toSyncableUser)
    .filter((user): user is SyncableUser => user !== null);
  const rest =
    page.records.length < ATTIO_PAGE_SIZE
      ? []
      : await fetchSyncableUsers(offset + page.records.length);
  return [...syncable, ...rest];
}

function toSyncableUser(
  record: AttioRecordFor<typeof AttioObject.User>,
): SyncableUser | null {
  const userId = record.user_id;
  const personRecordId = record.person?.target_record_id;
  if (!userId || !personRecordId) return null;
  return { userId, personRecordId };
}

function chunk<T>(items: readonly T[], size: number): T[][] {
  return Array.from({ length: Math.ceil(items.length / size) }, (_, index) =>
    items.slice(index * size, (index + 1) * size),
  );
}

interface SyncableUser {
  userId: string;
  personRecordId: string;
}
```
