Umbrella sources: one score, many senders

· scores, how-to, design

📅 May 23, 2026 · Tempo 1.0.6 · Leo from Caereforge

Most sources in Tempo are straightforward. Kopia is Kopia. Uptime Kuma is Uptime Kuma. One sender, one score, one row in the source panel.

But some sources are families. UniFi covers Network and Protect, very different signals under one vendor. Hazel can watch folders for new files, react to downloads, or pick up messages that a Mail.app rule exported to a folder. Scripts is a single banner over every Bash, Python, or AppleScript one-liner you point at Tempo. For these, one score per sender would be tedious and redundant. You’d end up maintaining dozens of nearly-identical JSON files that differ only in name.

That’s what umbrella sources solve.

The idea

An umbrella source is a single score that covers a whole family of senders. The score file lives at the parent level, scripts.json, com.noodlesoft.hazel.json, com.ubiquiti.unifi.json, and every sender whose provider identifier starts with that prefix inherits the parent’s configuration automatically.

Three umbrellas ship out of the box:

UmbrellaExample senders
Scriptsscripts.shell.check_disk, scripts.python.sensor_poll
Hazelcom.noodlesoft.hazel.mail, com.noodlesoft.hazel.scanner
UniFicom.ubiquiti.unifi.network, com.ubiquiti.unifi.protect

In the source panel, each sender still gets its own row, you see Shell, Python, Mail, Scanner as distinct sub-sources. But in the Score Editor, there’s one score. The parent defines the severity rules, the action buttons, and the grouping policy. Every child inherits all of it.

How the resolution works

When Tempo receives an event, it looks for a score matching the event’s provider identifier. The lookup walks up the dot-separated hierarchy until it finds a match:

  1. Look for scripts.shell.check_disk.json, not found.
  2. Drop the last segment: look for scripts.shell.json, not found.
  3. Drop again: look for scripts.json, found. Use it.

This is prefix walking. The most specific score wins, and the parent catches everything that doesn’t have a dedicated override. It’s the same resolution pattern regardless of how deep the identifier goes.

What the parent controls

A single umbrella score gives you quite a lot of control over how different senders behave, without needing separate score files:

Severity rules with metadata matching. The severityRules array can match on any metadata field. The bundled Scripts score uses metadata.keyword to assign severity per script type:

{
  "match": { "script_name": "backup_check", "keyword": "Failed" },
  "severity": "critical",
  "label": "Backup failed"
}

This makes backup_check events with keyword Failed render as critical, while a disk_usage event with keyword OK renders as green, same score, different presentation.

Conditional actions (new in 1.0.5). Individual severity rules can now carry their own action buttons. A critical match can surface an SSH button that a routine OK match doesn’t show:

{
  "match": { "keyword": "Critical" },
  "severity": "critical",
  "actions": [
    { "label": "SSH to host", "trigger": { "openURL": "ssh://${metadata.host}" } }
  ],
  "actionsMode": "extend"
}

The "extend" mode adds these buttons alongside the score’s default actions. Use "replace" to show only the rule-specific buttons when that rule matches.

Grouping. The grouping array controls how events stack in the timeline. Hazel groups by ["${metadata.rule}", "${metadata.folder}"], so events from the same rule acting on the same folder collapse into one row. Scripts groups by ["${metadata.script_name}"].

What requires a separate score

A few things live at the score level and apply uniformly to every sub-source under the umbrella:

If you need a specific sub-source to look visually distinct right now, drop a dedicated score file for it (see the escape hatch below). For severity, badge, and conditional actions, the umbrella score’s severityRules with metadata matching already covers per-sub-source differentiation without extra files.

The escape hatch

Drop a more specific score file alongside the umbrella, and it wins for that sender only:

~/Library/Application Support/Tempo/Scores/
  scripts.json                    ← umbrella (catches everything)
  scripts.shell.disk_check.json   ← override (just for this one script)

Events from scripts.shell.disk_check use the override. Events from every other scripts.* sender still fall through to the umbrella. You get per-source customization exactly where you need it, without maintaining a score file for every sender.

The same works for any umbrella. Drop com.noodlesoft.hazel.mail.json next to com.noodlesoft.hazel.json and Mail-rule events get their own colour and actions while everything else Hazel-driven stays on the parent’s defaults.

Tokens follow the same prefix logic

When you create a token in Settings → Ingestion, the provider field uses the same prefix binding. A token bound to scripts authorises every scripts.* sender. A token bound to scripts.shell only authorises Shell senders. The trade-off is convenience versus blast radius if the token leaks.

For umbrellas with many senders, Scripts especially, one token at the parent level is the practical choice. For umbrellas with two or three well-known children, UniFi Network and Protect, a token per child is worth the extra minute in Settings.

Writing your own umbrella

You’re not limited to the bundled umbrellas. Any score becomes an umbrella the moment a sender POSTs with a provider identifier that extends the score’s own identifier:

  1. Write com.example.mystack.json with your preferred colour, rules, and actions.
  2. Have your senders POST as com.example.mystack.web, com.example.mystack.api, com.example.mystack.worker.
  3. Each sender appears as its own sub-source in the panel, all inheriting the parent score.

No special flag, no configuration, the prefix hierarchy is implicit. If the identifiers share a prefix and a score exists at that prefix, you have an umbrella.

How sub-sources work today

Not all umbrellas handle sub-sources the same way in 1.0.x.

Hazel and UniFi let you create any sub-source freely. POST as com.noodlesoft.hazel.invoices or com.noodlesoft.hazel.receipts and each appears as its own named row under the Hazel parent in the source panel. Same for UniFi: com.ubiquiti.unifi.talk would show up alongside Network and Protect.

Scripts is more constrained. The source panel groups sub-sources by a fixed set of recognised languages: Shell, Python, and AppleScript. If you use scripts.shell.check_disk, it appears under “Shell”. But a custom second segment like scripts.backup.restic lands under “Other” instead of getting its own “Backup” row. The score resolution still works correctly, scripts.json catches all scripts.* senders regardless, but the source panel won’t reflect your naming.

User-created umbrellas (like com.example.mystack) work for score resolution via prefix walking, but their sub-sources appear as flat rows in the source panel rather than grouped under a parent.

V1.1 will remove these limitations. Source panel grouping will derive dynamically from your provider identifiers, with no hardcoded special cases.

What’s coming in V1.1

This post describes the umbrella model as it works in Tempo 1.0.x. Version 1.1 will ship a redesigned Score Editor that makes umbrella configuration more visual and more flexible, including per-sub-source card colour overrides and dynamic grouping driven entirely by your provider identifiers, with no hardcoded special cases. A new post will cover the V1.1 changes when they land.

Further reading

Leo from Caereforge