# Multiple triggers in one workflow

Combine incoming issue events with weekday reports using stable trigger keys.

[Deploy your workflow](/deploy)

[Start for free](https://app.whenever.dev/)

## One workflow, a webhook and five schedules

Records issue events as they arrive, and posts yesterday's totals every weekday morning.

```ts
import {
  defineStep,
  defineWorkflow,
  NonRetryableError,
  webhook,
  weekly,
  type WorkflowContext,
  type WorkflowManifest,
} from '@wix/whenever-workflow-sdk';

export const manifest: WorkflowManifest = {
  name: 'Weekday issue report',
  triggers: [
    webhook({ key: 'issue-events' }),
    weekly({ key: 'report-monday', day: 'monday', at: '08:30', tz: 'Europe/Vilnius' }),
    weekly({ key: 'report-tuesday', day: 'tuesday', at: '08:30', tz: 'Europe/Vilnius' }),
    weekly({ key: 'report-wednesday', day: 'wednesday', at: '08:30', tz: 'Europe/Vilnius' }),
    weekly({ key: 'report-thursday', day: 'thursday', at: '08:30', tz: 'Europe/Vilnius' }),
    weekly({ key: 'report-friday', day: 'friday', at: '08:30', tz: 'Europe/Vilnius' }),
  ],
};

type Input = Record<string, unknown> | void;
type Report = { day: string; opened: number; closed: number };

const vilnius = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Vilnius' });
const localDay = (value: number | string) => vilnius.format(new Date(value));

// Monday reports on Friday; every other weekday reports on the day before.
function reportDay(now: number, monday: boolean): string {
  const day = new Date(`${localDay(now)}T00:00:00Z`);
  day.setUTCDate(day.getUTCDate() - (monday ? 3 : 1));
  return day.toISOString().slice(0, 10);
}

export const recordEvent = defineStep(
  'Record issue event',
  async (ctx: WorkflowContext<Input>, day: string, action: 'opened' | 'closed', number: number) => {
    return await ctx.integrations.googlesheets.spreadsheetsValuesAppend({
      spreadsheetId: ctx.config.REPORT_SPREADSHEET_ID,
      range: 'Events!A:D',
      valueInputOption: 'RAW',
      values: [[day, action, number, ctx.trigger.deliveryId ?? '']],
    });
  },
);

export const countEvents = defineStep(
  'Count yesterday’s events',
  async (ctx: WorkflowContext<Input>) => {
    const day = reportDay(ctx.now(), ctx.trigger.key === 'report-monday');
    const read = await ctx.integrations.googlesheets.valuesGet({
      spreadsheet_id: ctx.config.REPORT_SPREADSHEET_ID,
      range: 'Events!A:B',
    });
    let opened = 0;
    let closed = 0;
    for (const row of read.values ?? []) {
      if (row[0] !== day) continue;
      if (row[1] === 'opened') opened++;
      if (row[1] === 'closed') closed++;
    }
    return { day, opened, closed };
  },
);

export const postReport = defineStep(
  'Post report to Slack',
  async (ctx: WorkflowContext<Input>, report: Report) => {
    const sent = 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: `Issues on ${report.day}: ${report.opened} opened, ${report.closed} closed.`,
      },
    });
    if (
      typeof sent.body !== 'object' ||
      sent.body === null ||
      !('ok' in sent.body) ||
      sent.body.ok !== true
    ) {
      throw new NonRetryableError('Slack did not accept the report');
    }
    return sent;
  },
);

export default defineWorkflow<
  Input,
  | { event: Awaited<ReturnType<typeof recordEvent>> }
  | { report: Report; slack: Awaited<ReturnType<typeof postReport>> }
  | { ignored: string }
>(async (ctx) => {
  if (ctx.trigger.type === 'webhook') {
    const input = ctx.input;
    if (typeof input !== 'object' || input === null) return { ignored: 'empty delivery' };
    const issue = input.issue;
    if (
      typeof issue !== 'object' ||
      issue === null ||
      !('number' in issue) ||
      typeof issue.number !== 'number'
    ) {
      throw new NonRetryableError('The issues event has no issue number');
    }
    if (
      input.action === 'opened' &&
      'created_at' in issue &&
      typeof issue.created_at === 'string'
    ) {
      return { event: await recordEvent(ctx, localDay(issue.created_at), 'opened', issue.number) };
    }
    if (input.action === 'closed' && 'closed_at' in issue && typeof issue.closed_at === 'string') {
      return { event: await recordEvent(ctx, localDay(issue.closed_at), 'closed', issue.number) };
    }
    return { ignored: 'neither opened nor closed' };
  }
  const report = await countEvents(ctx);
  const slack = await postReport(ctx, report);
  return { report, slack };
});
```

Every trigger carries its own key, and the run reads the one that fired from `ctx.trigger`: `type` tells the webhook path from the scheduled one, and `key` tells Monday, which reports on Friday, from the other weekdays. The webhook's `deliveryId` goes into the sheet with each event, so a delivery recorded twice can be told apart from two events.

## Before you publish

Read [what every example assumes](/docs/examples#what-every-example-assumes), the [trigger reference](/docs/triggers) and [runtime limits and responsibilities](/docs/runtime-limits). Validate each trigger path separately and keep the keys stable across edits.
