---
title: "How do you send a weekly summary on a fixed schedule?"
url: "https://palumb.com/use-cases/weekly-summary"
description: "One run stays open for the week, gathers each deadline as your app reports it, and sends a single summary on Monday. Then it opens next week's window itself."
---

[← Use cases](/use-cases/)

# How do you send a weekly summary on a fixed schedule?

One run stays open for the whole week and gathers what arrives. Your app reports each deadline as it appears, one call each, and every call lands in the run that is already waiting. Monday at nine the window closes, the run sends one summary, and before it ends it opens next week's window itself.

Three groups, one under the other, and a single vertical line running down outside them on the left that marks the flow of data. Nothing travels along that line. Instead the stretch between two ends fills, from whoever is calling toward whoever is called, holds while that one works, and then drains back the way it came. The fill is always the same grey, the ink of the Palumb mark: the line belongs to Palumb, and which end the fill starts from already says who placed the call. It carries a small stop at every height where something happens.

First group, yourapp.com/backend: your own application, and the calls it makes into Palumb: trigger for weeklyDeadlines on topic.user-91, sendEvent for task.due · Tue, sendEvent for task.due · Wed and sendEvent for task.due · Fri. Neither the bubble nor its mark exists until you make that call, and the mark is a small teal square that fills in once Palumb has it. While a call is in flight the bubble's border turns and the line below it fills down to Palumb.

Second group, palumb, the durable engine: the one that is not an address, because it is not something you run. It appears when the trigger arrives, and holds one bubble that says running workflow, except while it is waiting: then it names what it waits for, collecting task.due, with the elapsed count in front where the wait has a limit. At the end it says run complete. Its border turns for as long as the run lives, through every wait. Just under it the journal grows, one small violet token per step that has closed, each carrying that step's number.

One of those waits is a collection, and it is what this drawing is about: step week gathers 3 events before its window closes. While a window is open the bubble names the event it is gathering and counts what it already holds, and the count goes up at each arrival. Every arrival lights the bubble teal for a moment and starts nothing of yours: the line down to your code stays grey and empty. The step returns the whole lot at once, when the clock says so and not when the last event lands.

From the second group the fill runs down to the exact row that is about to run, and stops on it. It is not the channel of the call: your workflow is one signed endpoint, and the body replays from the top with the completed steps rehydrated until it reaches the row it has not run yet. The line is the flow of control, and control really does end up on that row.

Third group, yourapp.com/workflows/weeklyDeadlines: your workflow file itself, 12 lines of TypeScript, with its line numbers. In the margin of every line that calls a step there is a numbered circle, the way an editor marks a breakpoint: 1 for step.collect, 2 for step.send, 3 for step.trigger. At rest the margin holds only the line numbers; a step's number appears when that step starts running.

When the fill reaches a row, a band lights it, the way a debugger marks where it has stopped, and the ring in its margin turns. The moment the step is recorded the number leaves the margin, a tick takes its place, and the number reappears as a token in the row under Palumb: you wrote the step, we are the ones who remember it ran.

While the run waits the line down to your code is grey and empty and no row is lit, while the run stays alive in Palumb. That is the frame that matters: for as long as the wait lasts your server does nothing at all and ours does.

The drawing follows one path through the code. An if has two ways out and the animation takes the one this page is about; the other ways are in the file, which is printed in full below.

At rest, which is also what anyone who has asked for less motion sees, every step the run reached is ticked, the journal holds its 3 tokens, the bubble says run complete, and the line is empty: the drawing is the finished picture of this path.

weekly-deadlines.ts

```ts
workflow("weeklyDeadlines", async ({ subscriber, step }) => {
  const items = await step.collect("week", {
    event: "task.due",
    until: nextMonday("09:00", "Europe/Rome"),
  })

  if (items.length) {
    await step.send("digest", "email", () => deadlines(items))
  }

  return step.trigger("next", "weeklyDeadlines", subscriber.id)
})
```

## How it works

One trigger opens the first week (trigger). From there the run stays open and gathers (step 11) every deadline your app reports, three of them here, on the Tuesday, the Wednesday and the Friday. None of them wakes the run up: they land in it and it keeps waiting.

Monday at nine the window closes and one summary goes out with everything that arrived (step 22). Then, before ending, the run opens next week's window itself (step 33). One trigger is enough for good: the rhythm carries itself, and there is no schedule registered with us to keep in step with your code.

What Monday at nine means is computed in your repository, where the calendar and the reader's timezone are already known. The send also sits behind a condition of yours, because Monday arrives whether anything happened or not: a quiet week can be silence, or a message that says the week was quiet.

The other window: [one email for a burst of comments](/use-cases/comment-bursts/), where the window opens on the first event instead of on the calendar. Same step, one line of difference.

Everyone who joins gets in, and it costs nothing. Your invite goes out when the beta opens.

[Join the beta](/waitlist/)