getNodi

Templates / Weekly report

Generating the weekly report

A report that arrives before the week starts gets read and acted on. The same report on Friday afternoon is filed and forgotten, and the difference is entirely when it is sent.

The recipe

Request
POST https://{{domain}}/api/reports/weekly
Schedule
At 07:00 AM, only on Monday (0 7 * * 1)
Headers
Authorization: Bearer {{secret}}encrypted
Timeout
180s
Attempts
3
Counts as success
2xx

You supply

Your domain
example.com

Host only — no https:// and no trailing slash.

Shared secret
s3cr3t-valuestored encrypted

Whatever your endpoint checks the caller against.

Why this job exists

Weekly reporting tends to be assembled by hand, which makes it expensive and irregular. Somebody pulls numbers on a Friday when there is time, formats them, and sends them. It happens for a few months, then during a busy period it does not, and nobody chases it because nobody had come to rely on it arriving.

Regularity is most of the value. A report that reliably lands at the same hour becomes something people plan around; one that arrives when someone remembers is noise. The content usually matters less than the rhythm.

Automating it also changes what goes in. Once it is generated rather than typed, adding a metric costs one query rather than a permanent weekly chore, so reports become more useful over time instead of gradually shrinking to whatever is quickest to copy.

Setting it up

Compute over a closed period rather than relative to the moment the job runs. A report that measures "the last seven days" from execution time produces slightly different windows on any run that is delayed or retried, and the numbers stop reconciling with last week’s:

A closed, repeatable window

// Monday 00:00 to Sunday 23:59:59 of the week that just ended,
// in the report timezone rather than the server one.
const { start, end } = lastCompleteWeek(TIMEZONE);

const report = {
  period: { start, end },
  revenue: await revenueBetween(start, end),
  signups: await signupsBetween(start, end),
  churn: await churnBetween(start, end),
};

await mail.send('weekly-report', RECIPIENTS, report);
return Response.json(report);

Returning the report body has a useful side effect: the run history becomes a queryable archive of every week’s numbers, which is often the fastest way to answer "when did that start changing?".

Checking it actually works

  • Reconcile one week by hand against your analytics before trusting it. Do this once, properly.
  • Confirm the window does not overlap or leave gaps between consecutive weeks.
  • Check it renders in the mail clients your recipients actually use — a report nobody can read on a phone does not get read.

Where it goes wrong

A window relative to run time

Using "now minus seven days" means a retry an hour later measures a different week. Anchor to calendar boundaries in a fixed timezone.

Sending an empty or broken report

If a query fails, sending a report full of zeros is worse than sending nothing — people believe it. Fail the run and alert instead.

Too many numbers

A report with forty metrics is not read. Five, with last week beside each for comparison, is read every time.

Questions

Why Monday at 07:00?

It is waiting when the week starts without arriving over the weekend. Adjust to your team — the principle is that it should be there before the first meeting, not during it.

What if the report takes a long time to build?

The timeout here is three minutes. Beyond that, have the endpoint queue the build and send, and return immediately — then check the artefact with a second job.

Can it post to Slack instead of email?

Yes — point it at an incoming webhook, exactly as the Slack reminder recipe does. Many teams do both: a summary in Slack and the detail by mail.

Running it here

This is a preset in the product, not an illustration. Pick it in the dashboard, fill in the 2 values above, and the job is created, scheduled and enabled — with retries, a record of every run showing status, latency and response, and an alert the first time one fails. Secret values are encrypted at rest and never rendered back.

The free plan runs 3 jobs hourly and needs no card. Where a recipe wants a finer cadence than your plan allows, it is slowed to the fastest schedule you are permitted rather than rejected.