Skip to content
The Whenever SDK is open source.Explore the SDK on GitHub
On this page

Scheduled workflow examples

Complete workflows for an hourly inbox sweep and a daily analytics digest.

Sweep an inbox every hour

An interval trigger, a bound Gmail search that pages until it is done, and a batched write.

TypeScript
import {
  defineStep,
  defineWorkflow,
  every,
  manual,
  NonRetryableError,
  type WorkflowContext,
  type WorkflowManifest,
} from '@wix/whenever-workflow-sdk';

export const manifest: WorkflowManifest = {
  name: 'Mark notifier emails as read',
  triggers: [every('1h', { key: 'hourly-sweep' }), manual({ key: 'sweep-now' })],
};

export const findUnreadMessages = defineStep(
  'Find unread notifier emails',
  async (ctx: WorkflowContext) => {
    const ids: string[] = [];
    let pageToken: string | undefined;
    do {
      const page = await ctx.integrations.gmail.fetchEmails({
        user_id: 'me',
        query: `from:${ctx.config.NOTIFIER_ADDRESS} is:unread`,
        ids_only: true,
        max_results: 500,
        ...(pageToken ? { page_token: pageToken } : {}),
      });
      for (const message of page.messages ?? []) {
        if (!message.messageId) {
          throw new NonRetryableError('Gmail returned a message without an identifier');
        }
        ids.push(message.messageId);
      }
      pageToken = page.nextPageToken || undefined;
    } while (pageToken);
    return ids;
  },
);

export const markMessagesRead = defineStep(
  'Mark notifier emails as read',
  async (ctx: WorkflowContext, ids: string[]) => {
    const results = [];
    // batchModifyMessages accepts at most 1,000 ids per call.
    for (let offset = 0; offset < ids.length; offset += 1000) {
      const result = await ctx.integrations.gmail.batchModifyMessages({
        userId: 'me',
        messageIds: ids.slice(offset, offset + 1000),
        removeLabelIds: ['UNREAD'],
      });
      if (!result.success) {
        throw new NonRetryableError('Gmail did not confirm a batch was marked read');
      }
      results.push(result);
    }
    return results;
  },
);

export default defineWorkflow<
  void,
  { marked: number; results: Awaited<ReturnType<typeof markMessagesRead>> }
>(async (ctx) => {
  const ids = await findUnreadMessages(ctx);
  const results = await markMessagesRead(ctx, ids);
  return { marked: ids.length, results };
});

every('1h', { key }) fires on the hour, and manual({ key }) beside it lets you run the same code on demand. The sender to sweep is a ctx.config value, so the owner sets it in Setup and the source never names a person. The write checks Gmail's own success flag before counting a batch as done.

A daily analytics digest

Reads every Google Analytics property under one account, then posts the digest to Slack and emails it.

TypeScript
import {
  daily,
  defineStep,
  defineWorkflow,
  NonRetryableError,
  type WorkflowContext,
  type WorkflowManifest,
} from '@wix/whenever-workflow-sdk';

export const manifest: WorkflowManifest = {
  name: 'Google Analytics digest to Slack and Gmail',
  triggers: [daily({ key: 'morning-digest', at: '09:00', tz: 'UTC' })],
};

type Property = { property: string; name: string };

function utcDay(date: Date): string {
  return date.toISOString().slice(0, 10);
}

export const findProperties = defineStep(
  'Find Analytics properties',
  async (ctx: WorkflowContext) => {
    const properties: Property[] = [];
    let pageToken: string | undefined;
    do {
      const page = await ctx.integrations.google_analytics.listAccountSummaries(
        pageToken ? { pageToken } : {},
      );
      for (const account of page.accountSummaries ?? []) {
        if (account.account !== `accounts/${ctx.config.GOOGLE_ANALYTICS_ACCOUNT_ID}`) continue;
        for (const property of account.propertySummaries ?? []) {
          if (property.property) {
            properties.push({
              property: property.property,
              name: property.displayName ?? property.property,
            });
          }
        }
      }
      pageToken = page.nextPageToken;
    } while (pageToken);
    if (properties.length === 0) {
      throw new NonRetryableError(
        'No Analytics properties are visible under the configured account',
      );
    }
    return properties;
  },
);

export const readLastSevenDays = defineStep(
  'Read the last seven complete days',
  async (ctx: WorkflowContext, properties: Property[]) => {
    const now = new Date(ctx.now());
    const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
    const startDate = utcDay(new Date(today - 7 * 86_400_000));
    const endDate = utcDay(new Date(today - 86_400_000));
    const lines: string[] = [];
    for (const item of properties) {
      const report = await ctx.integrations.google_analytics.runReport({
        property: item.property,
        dateRanges: [{ startDate, endDate }],
        metrics: [{ name: 'activeUsers' }, { name: 'sessions' }, { name: 'screenPageViews' }],
      });
      // An explicit rowCount of 0 is a quiet week; a missing row without it is not evidence of one.
      const values =
        report.rowCount === 0
          ? ['0', '0', '0']
          : report.rows?.[0]?.metricValues?.map((metric) => metric.value);
      if (!values || values.length !== 3 || values.some((value) => value === undefined)) {
        throw new NonRetryableError(`Incomplete seven-day report for ${item.name}`);
      }
      lines.push(
        `• *${item.name}* — users ${values[0]} · sessions ${values[1]} · views ${values[2]}`,
      );
    }
    return `*Google Analytics, ${startDate} to ${endDate} (UTC)*\n${lines.join('\n')}`;
  },
);

export const postToSlack = defineStep(
  'Post digest to Slack',
  async (ctx: WorkflowContext, digest: string) => {
    const response = await ctx.integrations.http.post({
      url: 'https://slack.com/api/chat.postMessage',
      headers: { Authorization: `Bearer ${ctx.secrets.SLACK_BOT_TOKEN}` },
      body: { channel: ctx.config.SLACK_CHANNEL_ID, text: digest },
    });
    const body = response.body;
    if (typeof body !== 'object' || body === null || !('ok' in body) || body.ok !== true) {
      throw new NonRetryableError('Slack did not accept the message', {
        detail:
          typeof body === 'object' &&
          body !== null &&
          'error' in body &&
          typeof body.error === 'string'
            ? body.error
            : undefined,
        operation: 'chat.postMessage',
      });
    }
    return response;
  },
);

export const emailDigest = defineStep(
  'Email digest',
  async (ctx: WorkflowContext, digest: string) => {
    return await ctx.integrations.gmail.sendEmail({
      recipient_email: 'me',
      subject: 'Google Analytics — last 7 complete days',
      body: digest.replace(/[*_]/g, ''),
    });
  },
);

export default defineWorkflow<
  void,
  {
    digest: string;
    slack: Awaited<ReturnType<typeof postToSlack>>;
    email: Awaited<ReturnType<typeof emailDigest>>;
  }
>(async (ctx) => {
  const properties = await findProperties(ctx);
  const digest = await readLastSevenDays(ctx, properties);
  const slack = await postToSlack(ctx, digest);
  const email = await emailDigest(ctx, digest);
  return { digest, slack, email };
});

A schedule hands the run no input, so the date range comes from ctx.now() rather than from ctx.input. A report with an explicit row count of zero is a quiet week; a report with no rows and no count is incomplete, and the step throws rather than posting zeros it cannot vouch for. The Slack token is a workflow secret; Gmail and Analytics are connections the owner grants in Setup.

Before you publish

Read what every example assumes, the trigger reference and runtime limits and responsibilities. A scheduled poll starts a run even when it finds nothing to process.

Ready to deploy?