or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

api

features

charts

charts.mdconditional-formatting.mdvisualizations.md
authorization.mdchangesets.mdcharts-as-code.mdcompiler.mddashboards.mddbt.mdee-features.mdformatting.mdparameters.mdpivot.mdprojects-spaces.mdsql-runner.mdtemplating.mdwarehouse.md
index.md
tile.json

scheduler.mddocs/api/utilities/specialized/

Scheduler and Cron Utilities

Utilities for working with cron expressions, timezones, and scheduled tasks.

Capabilities

This module provides the following functionality:

Core Functions

/**
 * Get the timezone offset difference in minutes between two timezones
 * @param oldTz - Original timezone
 * @param newTz - New timezone
 * @returns Offset in minutes
 */
function getTzMinutesOffset(oldTz: string, newTz: string): number;

/**
 * Format minutes offset as human-readable string
 * @param offsetMins - Offset in minutes
 * @returns Formatted offset string (e.g., "+05:30", "-08:00")
 */
function formatMinutesOffset(offsetMins: number): string;

/**
 * Get human-readable timezone label with UTC offset
 * @param timezone - IANA timezone name (e.g., "America/New_York")
 * @returns Formatted label (e.g., "(UTC -05:00) America/New York")
 */
function getTimezoneLabel(timezone: string | undefined): string | undefined;

/**
 * Convert cron expression to human-readable format
 * @param cronExpression - Cron expression string
 * @param timezone - IANA timezone name
 * @returns Human-readable description
 */
function getHumanReadableCronExpression(
  cronExpression: string,
  timezone: string
): string;

/**
 * Validate that cron frequency is at least 1 hour
 * @param cronExpression - Cron expression to validate
 * @returns true if frequency is >= 1 hour
 */
function isValidFrequency(cronExpression: string): boolean;

/**
 * Validate timezone string
 * @param timezone - IANA timezone name to validate
 * @returns true if timezone is valid
 */
function isValidTimezone(timezone: string | undefined): boolean;

Examples

Basic Timezone Operations

import {
  getTzMinutesOffset,
  formatMinutesOffset,
  getTimezoneLabel,
  isValidTimezone,
} from '@lightdash/common';

// Get timezone offset difference
const offset = getTzMinutesOffset('America/New_York', 'America/Los_Angeles');
// Returns: -180 (3 hours difference)

// Format offset as string
const formatted = formatMinutesOffset(-300);
// Returns: "-05:00"

const formatted2 = formatMinutesOffset(330);
// Returns: "+05:30"

// Get timezone label with offset
const label = getTimezoneLabel('America/Los_Angeles');
// Returns: "(UTC -08:00) America/Los Angeles"

const label2 = getTimezoneLabel('Asia/Kolkata');
// Returns: "(UTC +05:30) Asia/Kolkata"

// Validate timezone
if (isValidTimezone('UTC')) {
  console.log('Valid timezone');
}

console.log(isValidTimezone('Invalid/Timezone')); // false
console.log(isValidTimezone('America/New_York')); // true

Cron Expression Utilities

import {
  getHumanReadableCronExpression,
  isValidFrequency,
} from '@lightdash/common';

// Convert cron to human-readable format
const readable = getHumanReadableCronExpression('0 9 * * *', 'America/New_York');
// Returns: "at 09:00 AM (UTC -05:00)"

const readable2 = getHumanReadableCronExpression('0 */2 * * *', 'UTC');
// Returns: "every 2 hours (UTC +00:00)"

const readable3 = getHumanReadableCronExpression('0 0 * * 1', 'Europe/London');
// Returns: "at 12:00 AM on Monday (UTC +00:00)"

// Validate cron frequency (must be >= 1 hour)
if (isValidFrequency('0 * * * *')) {
  console.log('Valid - runs hourly');
}

console.log(isValidFrequency('0 */2 * * *')); // true - every 2 hours
console.log(isValidFrequency('*/30 * * * *')); // false - every 30 minutes
console.log(isValidFrequency('0 0 * * *')); // true - daily

Scheduler Configuration

import {
  getHumanReadableCronExpression,
  isValidFrequency,
  isValidTimezone,
  getTimezoneLabel,
} from '@lightdash/common';

interface SchedulerConfig {
  cronExpression: string;
  timezone: string;
}

function validateSchedulerConfig(config: SchedulerConfig): string[] {
  const errors: string[] = [];

  // Validate timezone
  if (!isValidTimezone(config.timezone)) {
    errors.push('Invalid timezone');
  }

  // Validate frequency
  if (!isValidFrequency(config.cronExpression)) {
    errors.push('Schedule frequency must be at least 1 hour');
  }

  return errors;
}

function getSchedulerDescription(config: SchedulerConfig): string {
  return getHumanReadableCronExpression(
    config.cronExpression,
    config.timezone
  );
}

// Usage
const config: SchedulerConfig = {
  cronExpression: '0 9 * * *',
  timezone: 'America/New_York',
};

const errors = validateSchedulerConfig(config);
if (errors.length === 0) {
  const description = getSchedulerDescription(config);
  console.log(`Schedule: ${description}`);
  // Output: "Schedule: at 09:00 AM (UTC -05:00)"
}

Dashboard Scheduler

import {
  getHumanReadableCronExpression,
  getTimezoneLabel,
  isValidFrequency,
} from '@lightdash/common';

interface DashboardSchedule {
  dashboardId: string;
  cronExpression: string;
  timezone: string;
  recipients: string[];
}

async function createDashboardSchedule(
  schedule: DashboardSchedule
): Promise<void> {
  // Validate frequency
  if (!isValidFrequency(schedule.cronExpression)) {
    throw new Error('Schedule must run at least once per hour');
  }

  // Get human-readable description
  const description = getHumanReadableCronExpression(
    schedule.cronExpression,
    schedule.timezone
  );

  console.log(`Creating schedule: ${description}`);

  await database.schedules.create({
    ...schedule,
    description,
  });
}

// Usage
await createDashboardSchedule({
  dashboardId: 'dash-123',
  cronExpression: '0 8 * * 1-5', // Weekdays at 8 AM
  timezone: 'America/New_York',
  recipients: ['user@example.com'],
});

Timezone Selection UI

import { getTimezoneLabel, isValidTimezone } from '@lightdash/common';

// Get list of common timezones with labels
const commonTimezones = [
  'UTC',
  'America/New_York',
  'America/Chicago',
  'America/Denver',
  'America/Los_Angeles',
  'Europe/London',
  'Europe/Paris',
  'Asia/Tokyo',
  'Asia/Shanghai',
  'Australia/Sydney',
];

function getTimezoneOptions() {
  return commonTimezones
    .filter(isValidTimezone)
    .map(tz => ({
      value: tz,
      label: getTimezoneLabel(tz),
    }));
}

// Usage in React component
function TimezoneSelector({ value, onChange }: Props) {
  const options = getTimezoneOptions();

  return (
    <select value={value} onChange={(e) => onChange(e.target.value)}>
      {options.map(option => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

Schedule Migration

import { getTzMinutesOffset, formatMinutesOffset } from '@lightdash/common';

async function migrateScheduleTimezone(
  scheduleId: string,
  newTimezone: string
) {
  const schedule = await getSchedule(scheduleId);

  // Calculate time offset
  const offsetMinutes = getTzMinutesOffset(
    schedule.timezone,
    newTimezone
  );

  const offsetFormatted = formatMinutesOffset(offsetMinutes);

  console.log(`Migrating schedule from ${schedule.timezone} to ${newTimezone}`);
  console.log(`Time will shift by ${offsetFormatted}`);

  // Update schedule
  await database.schedules.update(scheduleId, {
    timezone: newTimezone,
    // Note: Cron expression stays the same, but will execute at different UTC time
  });
}

API Endpoints

import {
  getHumanReadableCronExpression,
  isValidFrequency,
  isValidTimezone,
} from '@lightdash/common';

// Create schedule endpoint
app.post('/api/schedules', async (req, res) => {
  const { cronExpression, timezone, dashboardId } = req.body;

  // Validate timezone
  if (!isValidTimezone(timezone)) {
    return res.status(400).json({
      error: 'Invalid timezone',
    });
  }

  // Validate frequency
  if (!isValidFrequency(cronExpression)) {
    return res.status(400).json({
      error: 'Schedule frequency must be at least 1 hour',
    });
  }

  // Get human-readable description
  const description = getHumanReadableCronExpression(
    cronExpression,
    timezone
  );

  const schedule = await createSchedule({
    cronExpression,
    timezone,
    dashboardId,
    description,
  });

  res.json(schedule);
});

// Get timezone list endpoint
app.get('/api/timezones', (req, res) => {
  const timezones = Intl.supportedValuesOf('timeZone')
    .filter(isValidTimezone)
    .map(tz => ({
      value: tz,
      label: getTimezoneLabel(tz),
    }));

  res.json(timezones);
});

Schedule Preview

import { getHumanReadableCronExpression } from '@lightdash/common';
// External dependency - Install separately: npm install cronstrue
import cronstrue from 'cronstrue';

function getSchedulePreview(
  cronExpression: string,
  timezone: string
): {
  readable: string;
  nextRuns: Date[];
} {
  // Get human-readable description
  const readable = getHumanReadableCronExpression(cronExpression, timezone);

  // Calculate next run times (using a cron library)
  const parser = require('cron-parser');
  const interval = parser.parseExpression(cronExpression, { tz: timezone });

  const nextRuns: Date[] = [];
  for (let i = 0; i < 5; i++) {
    nextRuns.push(interval.next().toDate());
  }

  return { readable, nextRuns };
}

// Usage
const preview = getSchedulePreview('0 9 * * 1-5', 'America/New_York');
console.log(`Schedule: ${preview.readable}`);
console.log('Next runs:', preview.nextRuns);

Testing

import {
  getTzMinutesOffset,
  formatMinutesOffset,
  getTimezoneLabel,
  isValidFrequency,
  isValidTimezone,
  getHumanReadableCronExpression,
} from '@lightdash/common';

describe('Scheduler utilities', () => {
  describe('timezone utilities', () => {
    it('should calculate timezone offset', () => {
      const offset = getTzMinutesOffset('UTC', 'America/New_York');
      expect(offset).toBeLessThan(0); // NY is behind UTC
    });

    it('should format offset', () => {
      expect(formatMinutesOffset(-300)).toBe('-05:00');
      expect(formatMinutesOffset(330)).toBe('+05:30');
    });

    it('should get timezone label', () => {
      const label = getTimezoneLabel('UTC');
      expect(label).toContain('UTC');
    });

    it('should validate timezone', () => {
      expect(isValidTimezone('UTC')).toBe(true);
      expect(isValidTimezone('Invalid/Zone')).toBe(false);
    });
  });

  describe('cron utilities', () => {
    it('should validate frequency', () => {
      expect(isValidFrequency('0 * * * *')).toBe(true); // Hourly
      expect(isValidFrequency('*/30 * * * *')).toBe(false); // Every 30 min
    });

    it('should get human readable cron', () => {
      const readable = getHumanReadableCronExpression('0 9 * * *', 'UTC');
      expect(readable).toContain('09:00');
    });
  });
});

Common Cron Expressions

ExpressionDescriptionValid?
0 * * * *Every hour
0 */2 * * *Every 2 hours
0 9 * * *Daily at 9 AM
0 9 * * 1-5Weekdays at 9 AM
0 0 * * 0Weekly on Sunday
*/30 * * * *Every 30 minutes✗ (too frequent)
*/15 * * * *Every 15 minutes✗ (too frequent)

Use Cases

  • Scheduled Reports: Schedule dashboard deliveries
  • Data Refreshes: Schedule data extract refreshes
  • Alert Schedules: Configure when alerts should run
  • Timezone Conversion: Handle user timezone preferences
  • Schedule Validation: Ensure schedules meet frequency requirements
  • UI Labels: Display human-readable schedule descriptions

Related Utilities

  • Date Formatting: See date utilities for date/time formatting
  • Validation: Part of broader validation framework
  • Constants: Session storage keys for scheduler filters