> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omni.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Cohort analysis using a thin spine

> Build cohort analyses using thin dimensional spines.

export const categoryIcons = {
  'administration': 'lock',
  'api': 'terminal',
  'connections': 'database',
  'dashboards': 'table-columns',
  'embed': 'code',
  'errors': 'exclamation',
  'modeling': 'wrench',
  'patterns': 'plus',
  'schedules & alerts': 'envelope',
  'visualizations': 'chart-column',
  'workbooks': 'book'
};

export const GuideSidebar = ({category, relatedLinks, updatedDate}) => {
  const [progress, setProgress] = React.useState(0);
  React.useEffect(() => {
    const sidebar = document.querySelector('.guide-sidebar');
    if (!sidebar) return;
    let container = sidebar.parentElement;
    while (container && !container.querySelector('.guide-header')) {
      container = container.parentElement;
    }
    if (container && !container.classList.contains('guide-page-layout')) {
      container.classList.add('guide-page-layout');
    }
  }, []);
  React.useEffect(() => {
    const handleScroll = () => {
      const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const scrollPercent = docHeight > 0 ? scrollTop / docHeight * 100 : 0;
      setProgress(Math.min(100, Math.max(0, scrollPercent)));
    };
    window.addEventListener('scroll', handleScroll, {
      passive: true
    });
    handleScroll();
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  const icon = category ? categoryIcons[category.toLowerCase()] || 'book' : 'book';
  return <aside className="guide-sidebar">
      <div className="guide-sidebar-content">
        <a href="/guides" className="guide-sidebar-back">
          <Icon icon="arrow-left" iconType="solid" size={14} />
          <span>All guides</span>
        </a>

        <div className="guide-sidebar-section">
          <div className="guide-sidebar-label">Progress</div>
          <div className="guide-sidebar-progress">
            <div className="guide-mascot">
              <svg viewBox="0 0 688 690" width="48" height="48">
                <defs>
                  <clipPath id="progressClip">
                    <rect x="0" y={0} width="688" height={progress * 6.9} />
                  </clipPath>
                </defs>

                {}
                <path d="M343.67 1.5C542.684 1.5 685.84 149.351 685.84 344.84C685.84 540.328 542.685 688.18 343.67 688.18C144.655 688.18 1.5 540.318 1.5 344.84C1.50007 149.361 144.655 1.50005 343.67 1.5Z" fill="#FCFCF7" stroke="#FF5FA2" strokeWidth="3" />

                {}
                <path d="M343.67 0C143.81 0 0 148.55 0 344.84C0 541.13 143.81 689.68 343.67 689.68C543.53 689.68 687.34 541.14 687.34 344.84C687.34 148.54 543.53 0 343.67 0Z" fill="#FF5FA2" clipPath="url(#progressClip)" />

                {}
                <path d="M337.89 319.29C337.89 336.75 322.49 350.14 302.81 349.83C286.18 349.57 273.89 337.29 274.37 321.45C274.88 304.82 290.91 290.88 309.98 290.44C325.69 290.09 337.88 302.69 337.88 319.29H337.89Z" fill="#4D122C" />
                <path d="M566.17 319.29C566.17 336.75 550.77 350.14 531.09 349.83C514.46 349.57 502.17 337.29 502.65 321.45C503.16 304.82 519.19 290.88 538.26 290.44C553.97 290.09 566.16 302.69 566.16 319.29H566.17Z" fill="#4D122C" />
                <path d="M367.74 342.07C360.22 346.32 359.4 354.9 370.62 366.4C381.85 377.9 399.76 389.56 420.81 389.18C441.88 389.1 460.67 377.72 472.47 363.53C473.83 361.93 478.84 356.88 478.51 351.07C478.32 348.35 476.17 341.19 467.83 341.38C463.46 341.44 461.21 343.68 456.69 347.36C445.2 356.14 432.7 361.21 420.56 361.27C408.43 361.43 395.68 356.17 385.39 347.22C380.32 342.81 375.25 337.82 367.74 342.07Z" fill="#4D122C" />
              </svg>
            </div>
            <span className="guide-sidebar-progress-text">{Math.round(progress)}%</span>
          </div>
        </div>

        {category && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Category</div>
            <div className="guide-sidebar-category">
              <Icon icon={icon} iconType="solid" size={14} />
              <span>{category}</span>
            </div>
          </div>}

        {updatedDate && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Last updated</div>
            <div className="guide-sidebar-date">{updatedDate}</div>
          </div>}

        {relatedLinks && relatedLinks.length > 0 && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Related</div>
            <ul className="guide-sidebar-links">
              {relatedLinks.map((link, index) => <li key={index}>
                  <a href={link.href}>{link.title}</a>
                </li>)}
            </ul>
          </div>}
      </div>
    </aside>;
};

export const GuideTitle = ({title}) => {
  return <div className="guide-header">
      <h1 className="guide-title">{title}</h1>
    </div>;
};

<GuideSidebar
  categoryIcons={categoryIcons}
  category="patterns"
  updatedDate="February 2026"
  relatedLinks={[
{ title: "Thin dimension spine pattern", href: "/guides/patterns/thin-dimension-spine" },
{ title: "Templated filters", href: "/modeling/templated-filters" },
{ title: "Measures", href: "/modeling/measures" },
{ title: "Topics", href: "/modeling/topics" }
]}
/>

<GuideTitle title="Cohort analysis using a thin spine" />

In the [Thin dimensional spine for multi-fact event analysis guide](/guides/patterns/thin-dimension-spine), you learned what a thin dimensional spine is, how it works, and how to implement your own using dbt and Omni.

In this guide, we'll build on the knowledge in that guide to walk you through several approaches to cohort analysis.

## Requirements

To follow along with this guide, you'll need:

* **To have read the [Thin dimensional spine for multi-fact event analysis guide](/guides/patterns/thin-dimension-spine).** This guide describes the basics of thin dimensional spines and how to build and materialize your own.
* **Familiarity with [modeling](/modeling) in Omni**

## Attribute-based cohorts

To build this type of analysis:

1. Maintain an attribute table with per-user first/last timestamps, e.g., `attr__users_event_bounds` with `first_signup_ts`, `first_purchase_ts`.
2. Group and filter users by these milestones across event streams

## Event-to-event cohorts

This type of analysis looks at the timing between events — for example, signup-to-first-login latency, or engagement patterns following a first purchase.

## Dynamic 'time since selected event' cohorting

Using the spine, you can model a dynamic "since basis" and "grain" selector to compute buckets of time since a specific event:

```yaml title="Snippet: dim__user_event_spine_thin.view" expandable theme={null}
dimensions:
  selected_basis_ts:
    type: timestamp
    sql: |
      case {{filters.dim__user_event_spine_thin.cohort_basis.value}}
        when 'first_ad' then ${attr__users_event_bounds.first_ad_ts}
        when 'first_signup' then ${attr__users_event_bounds.first_signup_ts}
        else ${attr__users_event_bounds.first_ad_ts}
      end

  time_since_selected_event:
    type: number
    label: Time Since Selected Event
    sql: |
      datediff(
        case {{filters.dim__user_event_spine_thin.cohort_grain.value}}
          when 'day' then 'day'
          else 'day'
        end,
        ${selected_basis_date},
        ${event_date_day}
      )
```
