> ## 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.

# Set up a custom calendar

> Group, filter, and compare dates by your own fiscal periods, such as a retail 4-5-4 calendar.

export const categoryIcons = {
  'administration': 'lock',
  'api': 'terminal',
  'connections': 'database',
  'dashboards': 'table-columns',
  'embed': 'code',
  'errors': 'exclamation',
  'migration': 'angles-right',
  '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="modeling"
  updatedDate="September 2026"
  relatedLinks={[
{ title: "custom_calendars parameter", href: "/modeling/models/custom-calendars" },
{ title: "custom_calendar dimension parameter", href: "/modeling/dimensions/parameters/custom-calendar" },
{ title: "fiscal_month_offset parameter", href: "/modeling/models/fiscal-month-offset" },
{ title: "Schema refreshes", href: "/modeling/develop/schema-refreshes" }
]}
/>

<GuideTitle title="Set up a custom calendar" />

Many retailers report on a 4-5-4 calendar: each quarter has 13 weeks, split into months of 4, 5, and 4 weeks. A custom calendar lets Omni group, filter, and compare a date dimension by those periods instead of Gregorian months and quarters.

This guide sets up a retail 4-5-4 calendar for a `sold_at` dimension. The same steps work for any calendar with one row per date, such as a 13-period or ISO-week fiscal calendar.

## When to use a custom calendar

Use a custom calendar when your periods do not start on the first day of a Gregorian month. Examples: a 4-5-4 or 5-4-4 retail calendar, a 13-period calendar, an ISO-week fiscal year, or a broadcast calendar.

Use [`fiscal_month_offset`](/modeling/models/fiscal-month-offset) instead when your fiscal year starts on the first day of a Gregorian month. A custom calendar needs a calendar table in your database, while `fiscal_month_offset` only shifts the start of the year. A custom calendar replaces the fiscal calendar for the dimensions that use it.

Keep these limits in mind before you choose a custom calendar:

* [Forecasting](/ai/forecasting) does not use the calendar.
* Period over period is not available on a query with two date fields, or with a sub-day timeframe.
* Date fill needs a date filter with a start and an end, or a named period such as `this year`.

## Requirements

To follow the steps in this guide, you'll need:

* **Modeler** or **Connection Admin** permissions for the connection, so you can edit the shared model and run a [schema refresh](/modeling/develop/schema-refreshes)
* **Permission to create a table in your database.** The calendar must be a database table. A [data input table (uploaded file)](/analyze-explore/data-input-csvs) only works if the connection's **Schema for table upload** property is defined, which pushes the table to the database.

<Steps titleSize="h2">
  <Step title="Create a calendar table" noAnchor>
    Create a table in your database with one row for each date. Include every date that your data can contain. Dates outside the table get `NULL` for each calendar timeframe.

    Each row holds the date and the periods that the date belongs to:

    | Column                 | Example      | Notes                                |
    | ---------------------- | ------------ | ------------------------------------ |
    | `calendar_date`        | `2024-03-01` | The Gregorian date. Must be unique.  |
    | `fy_start_date`        | `2024-02-04` | The first date of the retail year    |
    | `retail_quarter_start` | `2024-02-04` | The first date of the retail quarter |
    | `retail_month_start`   | `2024-02-04` | The first date of the retail month   |
    | `retail_week_start`    | `2024-02-25` | The first date of the retail week    |
    | `fiscal_year_number`   | `2024`       | The retail year as a number          |
    | `retail_quarter_num`   | `1`          | The quarter number in the year       |
    | `retail_month_num`     | `1`          | The month number in the year         |
    | `retail_month_name`    | `February`   | The month name                       |
    | `retail_week_num`      | `4`          | The week number in the year          |
    | `day_of_retail_year`   | `27`         | The day number in the year           |

    Period columns must hold the first date of the period, such as `2024-02-04`. Do not use a year number or a label such as `Q1`. Omni groups, sorts, and compares on that date.

    Only `calendar_date` and the columns you plan to map are required. A calendar that maps only `quarter` needs two columns.
  </Step>

  <Step title="Refresh the schema" noAnchor>
    Run a [schema refresh](/modeling/develop/schema-refreshes) so the calendar table appears in the model as a view. Note the view name that Omni assigns to the table. The view does not need to be in a topic.
  </Step>

  <Step title="Define the calendar in the model file" noAnchor>
    Add [`custom_calendars`](/modeling/models/custom-calendars) to the model file. Map each timeframe to a field in the calendar view:

    ```yaml title="model" theme={null}
    custom_calendars:
      retail_454:
        view: retail_calendar
        date: calendar_date
        mappings:
          year: fy_start_date
          quarter: retail_quarter_start
          month: retail_month_start
          week: retail_week_start
          year_num: fiscal_year_number
          quarter_of_year: retail_quarter_num
          month_num: retail_month_num
          month_name: retail_month_name
          week_of_year: retail_week_num
          day_of_year: day_of_retail_year
    ```

    With `year_num` mapped, each quarter displays as `2024-Q1`. Without it, each quarter displays its first date, such as `Feb 4, 2024`.
  </Step>

  <Step title="Apply the calendar to a dimension" noAnchor>
    In a view file, add [`custom_calendar`](/modeling/dimensions/parameters/custom-calendar) to each date or timestamp dimension that must use the calendar:

    ```yaml title="sales.view" theme={null}
    dimensions:
      sold_at:
        custom_calendar: retail_454
    ```

    <Warning>
      A custom calendar changes the results of saved content that uses the dimension. A `this quarter` filter or a period-over-period comparison now uses the calendar's quarter. Check saved content before you apply a calendar to a dimension.
    </Warning>
  </Step>

  <Step title="Check the result" noAnchor>
    Lastly, test the calendar in a workbook query:

    1. Add the **Quarter** timeframe of `sold_at` and a measure. Each row should show a retail quarter, such as `2024-Q1`. The first date of each row should match `retail_quarter_start` in your calendar table.
    2. Add a `sold_at` filter set to `this quarter`. The query should return the current retail quarter, not the Gregorian quarter.
  </Step>
</Steps>

## Next steps

* [`custom_calendars` parameter](/modeling/models/custom-calendars) for all properties and query behavior
* [`custom_calendar` dimension parameter](/modeling/dimensions/parameters/custom-calendar)
* [Period over period controls](/visualize-present/dashboards/controls#period-over-period) on dashboards
