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