Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions apps/api/src/workflows/schedules/send-summaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,95 @@ export class SendSummariesWorkflow extends WorkflowEntrypoint<
},
);

// Step 2.5: Check if we should skip sending due to empty data
const shouldSkip = await step.do("check-if-should-skip", async () => {
const scheduleConfig = initData.scheduleConfig as schema.ScheduleConfigSendSummaries;
const skipEmptyNotifications = scheduleConfig.skipEmptyNotifications ?? true;

if (!skipEmptyNotifications) {
return false; // Don't skip if the option is disabled
}

// Check if all summaries are empty (no generalSummary and no items in arrays)
const hasAnyData = generatedSummaries.some((summary) => {
const content = summary.content as any;

// Check if there's a general summary with content
if (content.generalSummary && content.generalSummary.trim().length > 0) {
return true;
}

// Check for any array content (userSummaries, repoSummaries, channelSummaries, etc.)
const arrayFields = [
"userSummaries",
"items",
"repoSummaries",
"projectSummaries",
"channelSummaries",
"teamSummaries",
];

for (const field of arrayFields) {
if (Array.isArray(content[field]) && content[field].length > 0) {
return true;
}
}

return false;
});

return !hasAnyData; // Skip if there's no data
});

// If we should skip, mark as completed and exit early
if (shouldSkip) {
await step.do("finalize-skipped-execution", async () => {
await db
.update(schema.scheduleRun)
.set({
status: "completed",
executionCount: initData.scheduleRunExecutionCount + 1,
executionMetadata: {
skipped: true,
reason: "No updates or activity found",
completedAt: new Date().toISOString(),
},
updatedAt: new Date(),
})
.where(eq(schema.scheduleRun.id, scheduleRunId));

// Schedule next execution if schedule is still active
if (initData.scheduleIsActive) {
const scheduleForNextExecution = {
id: initData.scheduleId,
config: initData.scheduleConfig,
name: initData.scheduleName,
isActive: initData.scheduleIsActive,
};

const nextExecutionTime = calculateNextScheduleExecution(scheduleForNextExecution as any);

if (nextExecutionTime) {
await db.insert(schema.scheduleRun).values({
id: generateId(),
scheduleId: initData.scheduleId,
createdByMemberId: initData.scheduleRunCreatedByMemberId,
status: "pending",
nextExecutionAt: nextExecutionTime,
executionCount: 0,
createdAt: new Date(),
updatedAt: new Date(),
});

console.log(`✅ Scheduled next execution for ${nextExecutionTime.toISOString()}`);
}
}

console.log("⏭️ Skipped sending summaries: No updates or activity found");
});
return; // Exit the workflow early
}

// Step 3: Resolve delivery targets
const deliveryTargets = await step.do("resolve-delivery-targets", async () => {
const config = initData.scheduleConfig as schema.ScheduleConfigSendSummaries;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
updateScheduleContract,
} from "@asyncstatus/api/typed-handlers/schedule";
import { Button } from "@asyncstatus/ui/components/button";
import { Checkbox } from "@asyncstatus/ui/components/checkbox";
import { Label } from "@asyncstatus/ui/components/label";
import {
Select,
SelectContent,
Expand Down Expand Up @@ -178,6 +180,28 @@ export function ActionFormContent(props: ActionFormContentProps) {
)}
</div>

{configName === "sendSummaries" && (
<FormField
control={form.control}
name="config.skipEmptyNotifications"
render={({ field }) => (
<div className="flex items-center gap-2">
<Checkbox
id="skip-empty-notifications"
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
<Label
htmlFor="skip-empty-notifications"
className="text-sm font-normal text-muted-foreground cursor-pointer"
>
Skip sending notification if no updates or activity found
</Label>
</div>
)}
/>
)}

{!isAddingGenerateFor && (
<FormField
control={form.control}
Expand Down Expand Up @@ -255,6 +279,10 @@ function getDefaultConfigBasedOnName(
previousConfig?.name === "remindToPostUpdates" || previousConfig?.name === "sendSummaries"
? previousConfig?.deliveryMethods
: [{ type: "organization", value: organizationSlug }],
skipEmptyNotifications:
previousConfig?.name === "sendSummaries"
? previousConfig?.skipEmptyNotifications ?? true
: true,
} satisfies ScheduleConfigSendSummaries;
}
}
5 changes: 5 additions & 0 deletions packages/db/src/schedule-config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export const ScheduleConfigSendSummaries = z.strictObject({
dayOfMonth: z.number().min(1).max(28).optional(), // 1-28 for monthly
summaryFor: z.array(ScheduleConfigSummaryFor.or(z.undefined())),
deliveryMethods: z.array(ScheduleConfigDeliveryMethod.or(z.undefined())),
skipEmptyNotifications: z.boolean().default(true), // Skip sending if no data/updates found
});
export type ScheduleConfigSendSummaries = z.infer<typeof ScheduleConfigSendSummaries>;

Expand Down Expand Up @@ -574,6 +575,10 @@ export const ScheduleConfigSendSummariesV3 = z3
.array(ScheduleConfigDeliveryMethodV3)
.default([])
.describe("Where summaries should be delivered (email, Slack, Discord, etc.)"),
skipEmptyNotifications: z3
.boolean()
.default(true)
.describe("Skip sending notifications if no data/updates are found"),
})
.strict()
.describe("Configuration for sending team summaries");
Expand Down