How to Send a Daily Status-Page Style Uptime Digest by Email
Build a Spojit workflow that runs every morning on a schedule, reads the prior day's monitoring check results from MongoDB, computes per-service uptime percentages, formats a clean status-page style HTML summary, and emails it to your stakeholders.
What This Integration Does
If you run uptime or health checks against your services and store each result, your team still needs a readable daily picture of how everything performed. This Spojit workflow turns a day's worth of raw check records into a single status-page style email: one row per service, an uptime percentage, and a quick "operational / degraded" read. It saves someone from logging into a dashboard every morning and lets non-technical stakeholders see the same numbers in their inbox.
The workflow runs unattended on a Schedule trigger (a daily cron in your timezone). On each run it queries the prior calendar day's check results from your monitoring collection in MongoDB, loops over each service to tally successful versus total checks, uses the math connector to turn those tallies into uptime percentages, builds an HTML table in a Transform node, and delivers it through a Send Email node. It writes nothing back to your database, so it is safe to re-run: a re-run simply rebuilds and resends the same digest from the same source data.
Prerequisites
- A MongoDB connection added in Spojit (Connections -> Add connection -> MongoDB) pointed at the database that holds your check results.
- A collection of check records (this tutorial assumes one named
checks) where each document has at least a service name, a success flag, and a timestamp, for exampleservice,ok(boolean), andcheckedAt(an ISO date string or date). - The recipients of the digest added to your org email allowlist under Settings -> General -> Email recipients, so the built-in Send Email node can deliver to them.
- A rough idea of how many distinct services you monitor (this drives the loop in Step 4).
Step 1: Add a Schedule trigger
Create a new workflow and set its trigger to Schedule. Add a 5-field Unix cron expression and an IANA timezone so the digest lands at a predictable local time. To send at 7am every day in Sydney, use:
Cron: 0 7 * * *
Timezone: Australia/Sydney
The trigger output is { scheduledAt }, which you will use to anchor "yesterday". A single Schedule trigger can hold multiple schedules if you later want a second send time, but one entry is enough here.
Step 2: Compute yesterday's date window
Add a Connector node in Direct mode on the date connector to derive the start and end of the prior day, so the workflow does not depend on a hardcoded date. First call subtract to step back one day from {{ trigger.scheduledAt }}, then call start-of and end-of with unit day to get the bounds. A clean approach is a single Connector node on the code connector using execute-javascript that returns both bounds as ISO strings:
const base = new Date(input.scheduledAt);
base.setUTCDate(base.getUTCDate() - 1);
const start = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate(), 0, 0, 0));
const end = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate(), 23, 59, 59));
return { start: start.toISOString(), end: end.toISOString(), label: start.toISOString().slice(0, 10) };
Pass {{ trigger.scheduledAt }} in as the input, and store the result as window. You now have {{ window.start }}, {{ window.end }}, and a human label {{ window.label }}.
Step 3: Read the prior day's check results from MongoDB
Add a Connector node in Direct mode on the mongodb connector and pick the find-documents tool. Set collection to checks and use a filter that matches only documents timestamped within yesterday's window:
{
"checkedAt": {
"$gte": "{{ window.start }}",
"$lte": "{{ window.end }}"
}
}
Add a sort of { "service": 1 } to group services together, and raise limit above the default of 100 if you record many checks per day (for example 10000). Store the result as results. The matched documents are available as {{ results.documents }}. If your collection name or field names differ, adjust the collection and filter keys to match your own schema.
Step 4: Tally checks per service and compute uptime with the math connector
You need one uptime figure per service. The cleanest way to group the raw records is a single Connector node on the code connector with execute-javascript that reduces the documents into per-service totals, passing {{ results.documents }} in as the input:
const byService = {};
for (const doc of input) {
const s = doc.service || "unknown";
byService[s] = byService[s] || { service: s, total: 0, ok: 0 };
byService[s].total += 1;
if (doc.ok === true) byService[s].ok += 1;
}
return { services: Object.values(byService) };
Store this as tallies, giving you {{ tallies.services }} with a service, total, and ok count each. Now add a Loop node in ForEach mode over {{ tallies.services }}. Inside the loop body, add a Connector node in Direct mode on the math connector using the percentage tool: set value to {{ item.ok }}, total to {{ item.total }}, and decimals to 2. Its result is the uptime percentage for that service. Collect each loop iteration's output (the service name plus its computed percentage) so the full list is available after the loop as the loop's output variable, for example {{ uptime }}.
If you prefer to compute everything in one place, you can instead do the division inside the same execute-javascript step and skip the loop, but using the math connector's percentage tool keeps the rounding consistent with the rest of your Spojit workflows.
Step 5: Build the status-page style HTML in a Transform node
Add a Transform node to shape the per-service uptime list into a single HTML string. Map over your uptime rows and emit a table where each row shows the service, its percentage, and a status word driven by a threshold (for example operational at 99 percent or above, degraded below that):
const rows = input.uptime.map(r => {
const pct = Number(r.percent);
const status = pct >= 99.9 ? "Operational"
: pct >= 99 ? "Minor"
: "Degraded";
return `<tr><td>${r.service}</td><td>${pct.toFixed(2)}%</td><td>${status}</td></tr>`;
}).join("");
return {
html: `<h2>Uptime digest for ${input.label}</h2>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th>Service</th><th>Uptime</th><th>Status</th></tr>
${rows}
</table>`
};
Feed the Transform node both the uptime list ({{ uptime }}) and the label ({{ window.label }}) as inputs, and store the output as digest. You now have {{ digest.html }} ready to send.
Step 6: Email the digest with a Send Email node
Add a Send Email node at the end. In Recipients, list your stakeholders comma-separated (each must be on the org email allowlist). Set Subject to something dated, such as Daily uptime digest - {{ window.label }}. Put the rendered summary in the Body field using {{ digest.html }}. Leave Reply-To as the workflow owner unless you want replies routed elsewhere, and set If sending fails to Fail the workflow so a missed digest shows up clearly in your execution history. Save and enable the workflow, and it will deliver every morning at your scheduled time.
Tips
- Keep the uptime threshold logic in the Transform node, not scattered across steps, so you can tune "operational vs degraded" in one place.
- If you monitor many services and check frequencies vary, also surface the raw
totalandokcounts in the table so a low percentage backed by only a handful of checks is obvious. - Use the math connector's
roundtool (fieldsnumberanddecimals) if you want a separate rounded figure for a headline "overall uptime" line above the table, computed from theaveragetool over all per-service percentages. - Ask Miraxa, the intelligent layer across your automation, to scaffold the canvas from a sentence such as "Add a Loop over {{ tallies.services }} with a math percentage node inside, then a Transform node that builds an HTML table," then fine-tune fields in the properties panel.
Common Pitfalls
- Timezone drift. The Schedule cron timezone and the date window in Step 2 must agree on what "yesterday" means. If your timestamps are stored in UTC but you schedule in
Australia/Sydney, decide deliberately whether the digest covers a UTC day or a local day, and build the window bounds to match. - Timestamp type mismatch. A string
$gte/$ltefilter only matches ifcheckedAtis stored as a comparable string; if your field is a real date type, ensure the values you pass compare correctly against it, or yourfind-documentscall returns zero documents. - Default query limit. The
find-documentslimitdefaults to 100. A busy day of checks will silently truncate unless you raise it, which understates totals and skews every percentage. - Empty days. If no checks ran (or all were filtered out), the per-service list is empty and the email body is just a header. Add a Condition node before Send Email to skip or flag a run with no data, rather than mailing a blank table.
Testing
Before scheduling it, run the workflow once with the Run button while the Schedule trigger is in place: each manual run uses the current time to compute "yesterday", so it exercises the same path. Temporarily narrow the find-documents filter to a single known service and confirm the percentage result matches a hand calculation of ok / total. Check the execution history to see the documents returned, the per-service tallies, and the final {{ digest.html }}. Send the first email to yourself only, confirm the table renders, then add the real recipient list and enable the schedule.